diff --git a/conductor/product-guidelines.md b/conductor/product-guidelines.md index 4c7f92a8..465ab8ba 100644 --- a/conductor/product-guidelines.md +++ b/conductor/product-guidelines.md @@ -25,6 +25,7 @@ - **Comprehensive Logging:** Aggressively log all actions, API payloads, tool calls, and executed scripts. Maintain timestamped JSON-L and markdown logs to ensure total transparency and debuggability. - **Mandatory ImGui Verification:** All changes to the GUI (`gui_2.py`) MUST be verified using the custom AST linter (`scripts/check_imgui_scopes.py`) to ensure all ImGui scopes (begin/end, push/pop) are properly matched. Developers should prioritize the use of `src/imgui_scopes.py` context managers (`imscope`) over manual push/pop calls. - **Modular Controller Pattern:** To prevent "God Object" bloat in core controllers (like `AppController`), all state-independent or utility logic must be moved to module-level functions. Functions requiring class state should accept the instance as an explicit dependency (`def logic(controller: AppController, ...)`). Massive `if/elif` dispatch blocks must be refactored into handler maps (dictionaries) of module-level functions. +- **UI Delegation for Hot-Reload:** All complex ImGui rendering logic must be extracted from the `App` class into module-level functions named `render_xxx(app: App)`. The `App` class should only contain thin delegation wrappers (`def _render_xxx(self): render_xxx(self)`). This architecture is mandatory for supporting state-preserving hot-reloads of the UI logic. - **Dependency Minimalism:** Limit external dependencies where possible. For instance, prefer standard library modules (like `urllib` and `html.parser` for web tools) over heavy third-party packages. ## Phase 5: Heavy Curation & Structural Integrity (MANDATORY) diff --git a/conductor/product.md b/conductor/product.md index 740b891a..aaf16e15 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -19,6 +19,7 @@ For deep implementation details when planning or implementing tracks, consult `d - **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI. - **Context & Memory Management:** Better visualization and management of token usage and context memory. Features an independent **Context Composition** panel decoupled from the project whitelist, with directory-grouped listings and per-file **View Modes** (Full, Summary, Skeleton, Outline, None). Includes a **Visual Slice Editor** for creating fuzzy-anchored line ranges with **Annotations** (tags and comments), and **View Presets** for saving named configurations of view settings. Features a dedicated **'Context' role** for manual injections and **Context Presets** for saving and loading complete compositions. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation. - **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation. +- **State-Preserving Hot Reload:** Supports selective, manual hot-reloading of Python modules (including the main GUI logic) via a delegation-based architecture. This allows for rapid UI iteration without losing application state or restarting the session. ## Key Features diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 07f4551a..1009ea12 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -83,6 +83,8 @@ - **Hybrid Shader Pipeline:** Utilizes an optimized `ImDrawList`-based batching technique to simulate UI effects (shadows, soft borders) instead of complex GPU post-processing, eliminating the overhead of heavy GPU-resident shaders. - **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation. - **UI Concern Isolation:** Enforces a strict separation between the GUI layer (Tkinter/ImGui) and the business logic (AppController). All platform-native UI actions, such as file and directory selection, are handled exclusively within the GUI layer. +- **UI Delegation Pattern:** Employs module-level rendering functions decoupled from the `App` class. This enables selective hot-reloading of UI logic by swapping function references at runtime while maintaining a stable state object (`app: App`). +- **Manual Hot-Reload Pipeline:** Implements a `HotReloader` utility that manages module invalidation and state preservation, triggered by keyboard shortcuts (Ctrl+Alt+R) or GUI controls. ter/ImGui) and the business logic (AppController). All platform-native UI actions, such as file and directory selection, are handled exclusively within the GUI layer. diff --git a/conductor/tracks.md b/conductor/tracks.md index 0000daa0..c65490db 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -79,7 +79,7 @@ This file tracks all major tracks for the project. Each track has its own detail --- -2. [ ] **Track: Hot Reload Python Codebase (Phase 2)** +2. [x] **Track: Hot Reload Python Codebase (Phase 2)** *Link: [./tracks/hot_reload_python_20260516/](./tracks/hot_reload_python_20260516/)* *Goal: Implement selective, state-preserving hot-reload for src/gui_2.py with delegation pattern refactor, manual trigger via Ctrl+Alt+R and GUI button, and visual error tint feedback on failure.* diff --git a/scripts/transform_render_methods_safe.py b/scripts/transform_render_methods_safe.py new file mode 100644 index 00000000..dc7d924e --- /dev/null +++ b/scripts/transform_render_methods_safe.py @@ -0,0 +1,63 @@ +from __future__ import annotations +import ast +import sys +import re + +def transform_file(file_path: str) -> None: + """Refactors App._render_xxx methods to module-level functions.""" + with open(file_path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + tree = ast.parse("\n".join(lines)) + app_class = next((n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "App"), None) + if not app_class: return + render_methods = [ + m for m in app_class.body + if isinstance(m, ast.FunctionDef) and m.name.startswith("_render_") and m.name != "_render_window_if_open" + ] + render_names = {m.name for m in render_methods} + render_methods.sort(key=lambda x: x.lineno, reverse=True) + extracted = [] + for m in render_methods: + s_idx = m.lineno - 1 + e_idx = m.end_lineno + if m.decorator_list: s_idx = m.decorator_list[0].lineno - 1 + m_lines = lines[s_idx:e_idx] + func_name = m.name.lstrip("_") + processed = [] + for l in m_lines: + processed.append(l[1:] if l.startswith(" ") else l) + def_line_idx = next((i for i, l in enumerate(processed) if l.strip().startswith("def ")), -1) + if def_line_idx != -1: + l = processed[def_line_idx] + l = l.replace(f"def {m.name}(", f"def {func_name}(", 1) + l = re.sub(r"\bself\b", "app: App", l, count=1) + processed[def_line_idx] = l + for i in range(def_line_idx + 1, len(processed)): + for m_name in render_names: + t_func = m_name.lstrip("_") + # Replace self._render_xxx( with render_xxx(app, + # then fix up (app, ) if it was empty. + processed[i] = processed[i].replace(f"self.{m_name}(", f"{t_func}(app, ") + processed[i] = processed[i].replace(f"(app, )", "(app)") + processed[i] = re.sub(r"(? 1: transform_file(sys.argv[1]) diff --git a/src/gui_2.py b/src/gui_2.py index 1c0c71c3..3ce74f90 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -48,6 +48,7 @@ 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 +from src.hot_reloader import HotReloader if sys.platform == "win32": import win32gui import win32con @@ -630,122 +631,20 @@ class App: # ---------------------------------------------------------------- helpers def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: - if imgui.button("[+]##" + str(id(content))) or force_open: - self.text_viewer_type = text_type - self.show_text_viewer = True - self.text_viewer_title = label - self.text_viewer_content = content + render_text_viewer(self, label, content, text_type, force_open) def _render_heavy_text(self, label: str, content: str, id_suffix: str = "") -> None: - imgui.text_colored(C_LBL, f"{label}:") - imgui.same_line() - if imgui.button("[+]##" + label + id_suffix): - self.show_text_viewer = True - self.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text' - self.text_viewer_title = label - self.text_viewer_content = content - - if not content: - imgui.text_disabled("(empty)") - return - - is_md = label in ("message", "text", "content") - ctx_id = f"heavy_{label}_{id_suffix}" - - with theme.ai_text_style(): - if len(content) > COMMS_CLAMP_CHARS: - if is_md: - imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar) - markdown_helper.render(content, context_id=ctx_id) - imgui.end_child() - else: - imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only) - else: - if is_md: - markdown_helper.render(content, context_id=ctx_id) - else: - if self.ui_word_wrap: - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(content) - imgui.pop_text_wrap_pos() - else: - imgui.text(content) + render_heavy_text(self, label, content, id_suffix) # ---------------------------------------------------------------- gui def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: - if not segments: - return - with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ - theme.ai_text_style(): - imgui.indent() - - show_content = True - if not is_standalone: - header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" - show_content = imgui.collapsing_header(header_label) - - if show_content: - h = 150 if is_standalone else 100 - with imscope.child(f"thinking_content_{entry_index}", 0, h, True): - for idx, seg in enumerate(segments): - content = seg.get("content", "") - marker = seg.get("marker", "thinking") - with imscope.id(f"think_{entry_index}_{idx}"): - imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") - if self.ui_word_wrap: - with imscope.text_wrap(imgui.get_content_region_avail().x): - imgui.text(content) - else: - imgui.text(content) - imgui.separator() - - imgui.unindent() + render_thinking_trace(self, segments, entry_index, is_standalone) def _render_selectable_label(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None: - with imscope.id(label + str(hash(value))): - with imscope.style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.frame_bg_hovered, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.frame_bg_active, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.border, vec4(0, 0, 0, 0)): - with imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0), \ - imscope.style_var(imgui.StyleVar_.frame_padding, imgui.ImVec2(0, 0)): - if color: - with imscope.style_color(imgui.Col_.text, color): - if multiline: - imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) - else: - if width > 0: imgui.set_next_item_width(width) - imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) - else: - if multiline: - imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) - else: - if width > 0: imgui.set_next_item_width(width) - imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + render_selectable_label(self, label, value, width, multiline, height, color) 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() + render_save_preset_modal(self) def _gui_func(self) -> None: io = imgui.get_io() @@ -785,94 +684,7 @@ class App: return def _render_main_interface(self) -> None: - self.perf_monitor.start_frame() - self._autofocus_response_tab = self.controller._autofocus_response_tab - - #region: Process GUI task queue - self._process_pending_gui_tasks() - self._process_pending_history_adds() - if self.controller._process_pending_tool_calls(): self._tool_log_dirty = True - #endregion: Process GUI task queue - - self._render_track_proposal_modal() - self._render_patch_modal() - self._render_base_prompt_diff_modal() - self._render_save_preset_modal() - self._render_save_workspace_profile_modal() - self._render_add_context_files_modal() - self._render_preset_manager_window() - self._render_tool_preset_manager_window() - self._render_persona_editor_window() - - # Auto-save (every 60s) - now = time.time() - if now - self._last_autosave >= self._autosave_interval: - self._last_autosave = now - try: - self._flush_to_project() - self._flush_to_config() - models.save_config(self.config) - except Exception: - pass # silent — don't disrupt the GUI loop - - # Sync pending comms - with self._pending_comms_lock: - if self._pending_comms: - if self.ui_auto_scroll_comms: self._scroll_comms_to_bottom = True - self._comms_log_dirty = True - for c in self._pending_comms: self._comms_log.append(c) - self._pending_comms.clear() - - if self.ui_focus_agent != self._last_ui_focus_agent: - self._comms_log_dirty = True - self._tool_log_dirty = True - self._last_ui_focus_agent = self.ui_focus_agent - - if self._comms_log_dirty: - if self.is_viewing_prior_session: self._comms_log_cache = self.prior_session_entries - else: - log_raw = list(self._comms_log) - if self.ui_focus_agent: self._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)] - else: self._comms_log_cache = log_raw - self._comms_log_dirty = False - - if self._tool_log_dirty: - if self.is_viewing_prior_session: self._tool_log_cache = self.prior_tool_calls - else: - log_raw = list(self._tool_log) - if self.ui_focus_agent: self._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(self.ui_focus_agent)] - else: self._tool_log_cache = log_raw - self._tool_log_dirty = False - - self._render_window_if_open("Project Settings", self._render_project_settings_hub) - self._render_window_if_open("Files & Media", self._render_files_and_media) - self._render_window_if_open("AI Settings", self._render_ai_settings_hub) - self._render_window_if_open("Usage Analytics", self._render_usage_analytics_panel, self.ui_separate_usage_analytics) - self._render_window_if_open("MMA Dashboard", self._render_mma_dashboard) - self._render_window_if_open("Task DAG", self._render_task_dag_panel, self.ui_separate_task_dag) - - self._render_window_if_open("Tier 1: Strategy", lambda: self._render_tier_stream_panel("Tier 1", "Tier 1"), self.ui_separate_tier1) - self._render_window_if_open("Tier 2: Tech Lead", lambda: self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)"), self.ui_separate_tier2) - self._render_window_if_open("Tier 3: Workers", lambda: self._render_tier_stream_panel("Tier 3", None), self.ui_separate_tier3) - self._render_window_if_open("Tier 4: QA", lambda: self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)"), self.ui_separate_tier4) - - if self.show_windows.get("Theme", False): self._render_theme_panel() - - self._render_window_if_open("Discussion Hub", self._render_discussion_hub) - self._render_window_if_open("Operations Hub", self._render_operations_hub) - - self._render_window_if_open("Message", self._render_message_panel, self.ui_separate_message_panel) - self._render_window_if_open("Response", self._render_response_panel, self.ui_separate_response_panel) - self._render_window_if_open("Tool Calls", self._render_tool_calls_panel, self.ui_separate_tool_calls_panel) - self._render_window_if_open("External Tools", self._render_external_tools_panel, self.ui_separate_external_tools) - self._render_window_if_open("Log Management", self._render_log_management) - self._render_window_if_open("Diagnostics", self._render_diagnostics_panel) - - self.perf_monitor.end_frame() - - # Modals / Popups - self._render_approve_script_modal() - self._render_mma_modals() + render_main_interface(self) 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.""" @@ -882,9 +694,7 @@ class App: if exp: render_func() 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 + render_custom_title_bar(self) def _show_menus(self) -> None: """ @@ -990,28 +800,7 @@ class App: imgui.pop_style_color() 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]]) -> None: - for i, entry in enumerate(reversed(history)): - actual_idx = len(history) - 1 - i - desc = entry.get("description", "UI Change") - ts = entry.get("timestamp", 0.0) - ts_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") - label = f"[{ts_str}] {desc}##{actual_idx}" - _, selected = imgui.selectable(label, False) - if selected: 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", 0, 0, True): - history = self.history.get_history() - if not history: imgui.text("No history available.") - else: iterate_history() + render_history_window(self) def _handle_history_logic(self) -> None: """ @@ -1073,489 +862,37 @@ class App: sys.stderr.flush() 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"]) - self.show_windows["Theme"] = bool(opened) - if exp: - imgui.text("Palette") - cp = theme.get_current_palette() - if imgui.begin_combo("##pal", cp): - for p in theme.get_palette_names(): - if imgui.selectable(p, p == cp)[0]: - theme.apply(p) - self._flush_to_config() - models.save_config(self.config) - 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) - 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 ch3: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel - imgui.separator() - imgui.text("Font") - imgui.push_item_width(-150) - ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) - imgui.pop_item_width() - if ch: theme._current_font_path = path - imgui.same_line() - if imgui.button("Browse##font"): - r = hide_tk_root() - p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) - r.destroy() - if p: theme._current_font_path = p - imgui.text("Size (px)") - imgui.same_line() - imgui.push_item_width(100) - ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") - if ch: theme._current_font_size = size - imgui.pop_item_width() - imgui.same_line() - if imgui.button("Apply Font (Requires Restart)"): - self._flush_to_config() - models.save_config(self.config) - self.ai_status = "Font settings saved. Restart required." - imgui.separator() - imgui.text("UI Scale (DPI)") - ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") - if ch: - theme.set_scale(scale) - self._flush_to_config() - models.save_config(self.config) - - imgui.text("Panel Transparency") - ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f") - if ch_t: - theme.set_transparency(trans) - self._flush_to_config() - models.save_config(self.config) - - imgui.text("Panel Item Transparency") - ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f") - if ch_ct: - theme.set_child_transparency(ctrans) - bg = bg_shader.get_bg() - ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled) - if ch_bg: - gui_cfg = self.config.setdefault("gui", {}) - gui_cfg["bg_shader_enabled"] = bg.enabled - self._flush_to_config() - models.save_config(self.config) - - ch_crt, self.ui_crt_filter = imgui.checkbox("CRT Filter", self.ui_crt_filter) - if ch_crt: - gui_cfg = self.config.setdefault("gui", {}) - gui_cfg["crt_filter_enabled"] = self.ui_crt_filter - self._flush_to_config() - models.save_config(self.config) - - imgui.end() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel") + render_theme_panel(self) 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) + render_shader_live_editor(self) #region: Diangostics & Analytics def _render_usage_analytics_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_usage_analytics_panel") - self._render_token_budget_panel() - imgui.separator() - self._render_cache_panel() - imgui.separator() - self._render_tool_analytics_panel() - imgui.separator() - self._render_session_insights_panel() - imgui.separator() - - # 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') - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_usage_analytics_panel") + render_usage_analytics_panel(self) 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") + render_cache_panel(self) 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") + render_diagnostics_panel(self) def _render_tool_analytics_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_analytics_panel") - imgui.text_colored(C_LBL, 'Tool Usage') - imgui.separator() - now = time.time() - if not hasattr(self, '_tool_stats_cache_time') or now - self._tool_stats_cache_time > 1.0: - self._cached_tool_stats = getattr(self.controller, '_tool_stats', {}) - tool_stats = getattr(self.controller, '_cached_tool_stats', {}) - if not tool_stats: - imgui.text_disabled("No tool usage data") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_analytics_panel") - return - if imgui.begin_table("tool_stats", 4, imgui.TableFlags_.borders | imgui.TableFlags_.sortable): - imgui.table_setup_column("Tool") - imgui.table_setup_column("Count") - imgui.table_setup_column("Avg (ms)") - imgui.table_setup_column("Fail %") - imgui.table_headers_row() - sorted_tools = sorted(tool_stats.items(), key=lambda x: -x[1].get("count", 0)) - for tool_name, stats in sorted_tools: - count = stats.get("count", 0) - total_time = stats.get("total_time_ms", 0) - failures = stats.get("failures", 0) - avg_time = total_time / count if count > 0 else 0 - fail_pct = (failures / count * 100) if count > 0 else 0 - imgui.table_next_row() - imgui.table_set_column_index(0) - imgui.text(tool_name) - imgui.table_set_column_index(1) - imgui.text(str(count)) - imgui.table_set_column_index(2) - imgui.text(f"{avg_time:.0f}") - imgui.table_set_column_index(3) - if fail_pct > 0: imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{fail_pct:.0f}%") - else: imgui.text("0%") - imgui.end_table() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_analytics_panel") + render_tool_analytics_panel(self) def _render_token_budget_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_token_budget_panel") - imgui.text_colored(C_LBL, 'Prompt Utilization') - usage = self.session_usage - total = usage["input_tokens"] + usage["output_tokens"] - if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"] - self._render_selectable_label("session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES) - if usage.get("last_latency", 0.0) > 0: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s") - if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") - if self._gemini_cache_text: imgui.text_colored(C_SUB, self._gemini_cache_text) - imgui.separator() - - if self._token_stats_dirty: - self._token_stats_dirty = False - # Offload to background thread via event queue - self.controller.event_queue.put("refresh_api_metrics", {"md_content": self._last_stable_md or ""}) - stats = self._token_stats - if not stats: - imgui.text_disabled("Token stats unavailable") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_token_budget_panel") - return - pct = stats.get("utilization_pct", 0.0) - current = stats.get("estimated_prompt_tokens", stats.get("total_tokens", 0)) - limit = stats.get("max_prompt_tokens", 0) - headroom = stats.get("headroom_tokens", max(0, limit - current)) - if pct < 50.0: color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) - elif pct < 80.0: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) - else: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) - imgui.push_style_color(imgui.Col_.plot_histogram, color) - imgui.progress_bar(pct / 100.0, imgui.ImVec2(-1, 0), f"{pct:.1f}%") - imgui.pop_style_color() - imgui.text_disabled(f"{current:,} / {limit:,} tokens ({headroom:,} remaining)") - sys_tok = stats.get("system_tokens", 0) - tool_tok = stats.get("tools_tokens", 0) - hist_tok = stats.get("history_tokens", 0) - total_tok = sys_tok + tool_tok + hist_tok or 1 - if imgui.begin_table("token_breakdown", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): - imgui.table_setup_column("Component") - imgui.table_setup_column("Tokens") - imgui.table_setup_column("Pct") - imgui.table_headers_row() - for lbl, tok in [("System", sys_tok), ("Tools", tool_tok), ("History", hist_tok)]: - imgui.table_next_row() - imgui.table_set_column_index(0); imgui.text(lbl) - imgui.table_set_column_index(1); imgui.text(f"{tok:,}") - imgui.table_set_column_index(2); imgui.text(f"{tok / total_tok * 100:.0f}%") - imgui.end_table() - imgui.separator() - imgui.text("MMA Tier Costs") - if hasattr(self, 'mma_tier_usage') and self.mma_tier_usage: - if imgui.begin_table("tier_cost_breakdown", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): - imgui.table_setup_column("Tier") - imgui.table_setup_column("Model") - imgui.table_setup_column("Tokens") - imgui.table_setup_column("Est. Cost") - imgui.table_headers_row() - for tier, stats in self.mma_tier_usage.items(): - model = stats.get('model', 'unknown') - in_t = stats.get('input', 0) - out_t = stats.get('output', 0) - tokens = in_t + out_t - cost = cost_tracker.estimate_cost(model, in_t, out_t) - imgui.table_next_row() - imgui.table_set_column_index(0); self._render_selectable_label(f"tier_{tier}", tier, width=-1) - imgui.table_set_column_index(1); self._render_selectable_label(f"model_{tier}", model.split("-")[0], width=-1) - imgui.table_set_column_index(2); self._render_selectable_label(f"tokens_{tier}", f"{tokens:,}", width=-1) - imgui.table_set_column_index(3); self._render_selectable_label(f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1)) - imgui.end_table() - tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in self.mma_tier_usage.values()) - self._render_selectable_label("session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1)) - else: - imgui.text_disabled("No MMA tier usage data") - if stats.get("would_trim"): - imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.0, 1.0), "WARNING: Next call will trim history") - trimmable = stats.get("trimmable_turns", 0) - if trimmable: imgui.text_disabled(f"Trimmable turns: {trimmable}") - msgs = stats.get("messages") - if msgs: - shown = 0 - for msg in msgs: - if shown >= 3: break - if msg.get("trimmable"): - role = msg.get("role", "?") - toks = msg.get("tokens", 0) - imgui.text_disabled(f" [{role}] ~{toks:,} tokens") - shown += 1 - imgui.separator() - cache_stats = getattr(self.controller, '_cached_cache_stats', {}) - if cache_stats.get("cache_exists"): - age = cache_stats.get("cache_age_seconds", 0) - ttl = cache_stats.get("ttl_seconds", 3600) - imgui.text_colored(C_LBL, f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s") - else: - imgui.text_disabled("Cache Usage: INACTIVE") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_token_budget_panel") + render_token_budget_panel(self) def _render_session_insights_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_session_insights_panel") - imgui.text_colored(C_LBL, 'Session Insights') - imgui.separator() - insights = self.controller.get_session_insights() - imgui.text(f"Total Tokens: {insights.get('total_tokens', 0):,}") - imgui.text(f"API Calls: {insights.get('call_count', 0)}") - imgui.text(f"Burn Rate: {insights.get('burn_rate', 0):.0f} tokens/min") - imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}") - completed = insights.get('completed_tickets', 0) - efficiency = insights.get('efficiency', 0) - imgui.text(f"Completed: {completed}") - imgui.text(f"Tokens/Ticket: {efficiency:.0f}" if efficiency > 0 else "Tokens/Ticket: N/A") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_session_insights_panel") + render_session_insights_panel(self) #endregion: Diangostics & Analytics #region: Logging def _render_log_management(self) -> None: - """ - [C: tests/test_log_management_ui.py:test_render_log_management_logic] - """ - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management") - 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")) - else: - 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() - imgui.same_line() - 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): - imgui.table_setup_column("Session ID") - imgui.table_setup_column("Start Time") - imgui.table_setup_column("Star") - imgui.table_setup_column("Reason") - imgui.table_setup_column("Size (KB)") - imgui.table_setup_column("Msgs") - imgui.table_setup_column("Actions") - imgui.table_headers_row() - for session_id, s_data in sessions.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(session_id) - imgui.table_next_column() - imgui.text(s_data.get("start_time", "")) - imgui.table_next_column() - whitelisted = s_data.get("whitelisted", False) - if whitelisted: - imgui.text_colored(vec4(255, 215, 0), "YES") - else: - imgui.text("NO") - metadata = s_data.get("metadata") or {} - imgui.table_next_column() - imgui.text(metadata.get("reason", "")) - imgui.table_next_column() - imgui.text(str(metadata.get("size_kb", ""))) - imgui.table_next_column() - imgui.text(str(metadata.get("message_count", ""))) - imgui.table_next_column() - if imgui.button(f"Load##{session_id}"): - self.cb_load_prior_log(s_data.get("path")) - imgui.same_line() - if whitelisted: - if imgui.button(f"Unstar##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=False, - reason=str(metadata.get("reason") or "") - ) - else: - if imgui.button(f"Star##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=True, - reason="Manually whitelisted" - ) - imgui.end_table() - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_log_management") + render_log_management(self) def cb_load_prior_log(self, path: Optional[str] = None) -> None: if path is None: @@ -1570,165 +907,19 @@ class App: #region: Project Management 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() + render_project_settings_hub(self) 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) - imgui.text_colored(C_IN, f"Active: {proj_name}") - imgui.separator() - imgui.text("Execution Mode") - modes = ["native", "beads"] - current_idx = modes.index(self.ui_project_execution_mode) if self.ui_project_execution_mode in modes else 0 - ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) - if ch: self.ui_project_execution_mode = modes[new_idx] - imgui.separator() - imgui.text("Git Directory") - ch, self.ui_project_git_dir = imgui.input_text("##git_dir", self.ui_project_git_dir) - imgui.same_line() - if imgui.button("Browse##git"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Git Directory") - r.destroy() - if d: self.ui_project_git_dir = d - imgui.separator() - imgui.text("Output Dir") - ch, self.ui_output_dir = imgui.input_text("##out_dir", self.ui_output_dir) - imgui.same_line() - if imgui.button("Browse##out"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Output Dir") - r.destroy() - if d: self.ui_output_dir = d - imgui.separator() - imgui.text("Conductor Directory") - ch, self.ui_project_conductor_dir = imgui.input_text("##cond_dir", self.ui_project_conductor_dir) - imgui.same_line() - if imgui.button("Browse##cond"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Conductor Directory") - r.destroy() - if d: self.ui_project_conductor_dir = d - imgui.separator() - imgui.text("Project Files") - imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) - for i, pp in enumerate(self.project_paths): - is_active = (pp == self.active_project_path) - if imgui.button(f"x##p{i}"): - removed = self.project_paths.pop(i) - if removed == self.active_project_path and self.project_paths: self._switch_project(self.project_paths[0]) - break - imgui.same_line() - marker = " *" if is_active else "" - if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) - if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): self._switch_project(pp) - if is_active: imgui.pop_style_color() - imgui.same_line() - imgui.text_colored(C_LBL, pp) - imgui.end_child() - if imgui.button("Add Project"): - r = hide_tk_root() - p = filedialog.askopenfilename( - title="Select Project .toml", - filetypes=[("TOML", "*.toml"), ("All", "*.*")], - ) - r.destroy() - if p and p not in self.project_paths: - self.project_paths.append(p) - imgui.same_line() - if imgui.button("New Project"): - r = hide_tk_root() - p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) - r.destroy() - if p: - name = Path(p).stem - proj = project_manager.default_project(name) - project_manager.save_project(proj, p) - if p not in self.project_paths: self.project_paths.append(p) - self._switch_project(p) - imgui.same_line() - if imgui.button("Save All"): - self._flush_to_project() - self._flush_to_config() - models.save_config(self.config) - self.ai_status = "config saved" - ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap) - ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms) - 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") + render_projects_panel(self) def _render_paths_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") - path_info = paths.get_full_path_info() - - imgui.text_colored(C_IN, "System Path Configuration") - imgui.separator() + render_paths_panel(self) def _render_path_field(label: str, attr: str, key: str, tooltip: str): - info = path_info.get(key, {'source': 'unknown'}) - imgui.text(label) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - imgui.same_line() - imgui.text_disabled(f"(Source: {info['source']})") - - val = getattr(self, attr) - changed, new_val = imgui.input_text(f"##{key}", val) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - if changed: setattr(self, attr, new_val) - imgui.same_line() - if imgui.button(f"Browse##{key}"): - r = hide_tk_root() - d = filedialog.askdirectory(title=f"Select {label}") - r.destroy() - if d: setattr(self, attr, d) + render_path_field(self, label, attr, key, tooltip) def _render_external_tools_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_external_tools_panel") - if imgui.button("Refresh External MCPs"): self.event_queue.put("refresh_external_mcps", None) - - imgui.separator() - - # Server status indicators - manager = mcp_client.get_external_mcp_manager() - statuses = manager.get_servers_status() - if statuses: - imgui.text("Servers:") - for sname, status in statuses.items(): - imgui.same_line() - # Green for running, Yellow for starting, Red for error, Gray for idle - col = (0.5, 0.5, 0.5, 1.0) - if status == 'running': col = (0.0, 1.0, 0.0, 1.0) - elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0) - elif status == 'error': col = (1.0, 0.0, 0.0, 1.0) - imgui.color_button(f"##status_{sname}", col) - imgui.same_line() - imgui.text(sname) - imgui.separator() - - tools = manager.get_all_tools() - if not tools: - imgui.text_disabled("No external tools found.") - else: - if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Name") - imgui.table_setup_column("Server") - imgui.table_setup_column("Description") - imgui.table_headers_row() - - for tname, tinfo in tools.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(tname) - imgui.table_next_column() - imgui.text(tinfo.get('server', 'unknown')) - imgui.table_next_column() - imgui.text(tinfo.get('description', '')) - imgui.end_table() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_external_tools_panel") + render_external_tools_panel(self) def _set_external_editor_default(self, editor_name: str) -> None: from src import models @@ -1770,1245 +961,65 @@ class App: #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() + render_ai_settings_hub(self) 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') + render_rag_panel(self) 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)) + render_system_prompts_panel(self) 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() + render_agent_tools_panel(self) 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 - - if imgui.begin_table("prompt_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row() - - # Left Sidebar - imgui.table_next_column() - imgui.begin_child("prompt_list_pane", imgui.ImVec2(0, 0), False) - if True: - if imgui.button("New Preset", imgui.ImVec2(-1, 0)): - self._editing_preset_name = "" - self._editing_preset_system_prompt = "" - self._editing_preset_scope = "project" - self._selected_preset_idx = -1 - imgui.separator() - preset_names = sorted(self.controller.presets.keys()) - for i, name in enumerate(preset_names): - if name and imgui.selectable(f"{name}##p_{i}", self._selected_preset_idx == i)[0]: - self._selected_preset_idx = i - self._editing_preset_name = name - p = self.controller.presets[name] - self._editing_preset_system_prompt = p.system_prompt - self._editing_preset_scope = self.controller.preset_manager.get_preset_scope(name) - imgui.end_child() - - # Right Editor - imgui.table_next_column() - avail_r = imgui.get_content_region_avail() - imgui.begin_child("prompt_edit_pane", imgui.ImVec2(0, avail_r.y - 45), False) - if True: - p_disp = self._editing_preset_name or "(New Preset)" - imgui.text_colored(C_IN, f"Editing Prompt Preset: {p_disp}") - imgui.separator() - - if imgui.begin_table("p_meta", 2): - imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row() - imgui.table_next_column(); imgui.text("Name:") - imgui.table_next_column(); imgui.set_next_item_width(-1) - _, self._editing_preset_name = imgui.input_text("##epn", self._editing_preset_name) - imgui.table_next_row() - imgui.table_next_column(); imgui.text("Scope:") - imgui.table_next_column() - if imgui.radio_button("Global##ps", self._editing_preset_scope == "global"): self._editing_preset_scope = "global" - imgui.same_line() - if imgui.radio_button("Project##ps", self._editing_preset_scope == "project"): self._editing_preset_scope = "project" - imgui.end_table() - - imgui.dummy(imgui.ImVec2(0, 4)) - imgui.separator() - imgui.text("Prompt Content:") - imgui.same_line() - if imgui.button("Pop out MD Preview"): - self.text_viewer_title = f"Preset: {self._editing_preset_name}" - self.text_viewer_content = self._editing_preset_system_prompt - self.text_viewer_type = "markdown" - self.show_text_viewer = True - - rem_y = imgui.get_content_region_avail().y - _, self._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", self._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y)) - imgui.end_child() - - # Footer Buttons - imgui.separator() - imgui.dummy(imgui.ImVec2(0, 4)) - if imgui.button("Save##p", imgui.ImVec2(100, 0)): - if self._editing_preset_name.strip(): - self.controller._cb_save_preset( - self._editing_preset_name.strip(), - self._editing_preset_system_prompt, - self._editing_preset_scope - ) - self.ai_status = f"Saved: {self._editing_preset_name}" - imgui.same_line() - if imgui.button("Delete##p", imgui.ImVec2(100, 0)): - if self._editing_preset_name: - self.controller._cb_delete_preset(self._editing_preset_name, self._editing_preset_scope) - self._editing_preset_name = "" - self._selected_preset_idx = -1 - if not is_embedded: - imgui.same_line() - if imgui.button("Close##p", imgui.ImVec2(100, 0)): self.show_preset_manager_window = False - imgui.end_table() + render_preset_manager_content(self, is_embedded) def _render_preset_manager_window(self, is_embedded: bool = False) -> None: - if not self.show_preset_manager_window and not is_embedded: return - if not is_embedded: - imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) - with imscope.window("Prompt Presets Manager", self.show_preset_manager_window) as (opened, visible): - self.show_preset_manager_window = visible - if opened: self._render_preset_manager_content(is_embedded=is_embedded) - else: - self._render_preset_manager_content(is_embedded=is_embedded) + render_preset_manager_window(self, is_embedded) def _render_tool_preset_manager_content(self, is_embedded: bool = False) -> None: - avail = imgui.get_content_region_avail() - if not hasattr(self, "_tool_split_v"): self._tool_split_v = 0.4 - if not hasattr(self, "_bias_split_v"): self._bias_split_v = 0.6 - if not hasattr(self, "_tool_list_open"): self._tool_list_open = True - if not hasattr(self, "_bias_list_open"): self._bias_list_open = True - if not hasattr(self, "_bias_weights_open"): self._bias_weights_open = True - if not hasattr(self, "_bias_cats_open"): self._bias_cats_open = True - - if imgui.begin_table("tp_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row() - - # Left Sidebar - imgui.table_next_column() - imgui.begin_child("tp_list_pane", imgui.ImVec2(0, 0), False) - if True: - if imgui.button("New Preset", imgui.ImVec2(-1, 0)): - self._editing_tool_preset_name = ""; self._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES} - self._editing_tool_preset_scope = "project"; self._selected_tool_preset_idx = -1 - imgui.separator() - preset_names = sorted(self.controller.tool_presets.keys()) - for i, name in enumerate(preset_names): - if name and imgui.selectable(f"{name}##tp_{i}", self._selected_tool_preset_idx == i)[0]: - self._selected_tool_preset_idx = i; self._editing_tool_preset_name = name - preset = self.controller.tool_presets[name] - self._editing_tool_preset_categories = {cat: copy.deepcopy(tools) for cat, tools in preset.categories.items()} - imgui.end_child() - - # Right Editor - imgui.table_next_column() - avail_r = imgui.get_content_region_avail() - imgui.begin_child("tp_editor_content", imgui.ImVec2(0, avail_r.y - 45), False) - if True: - p_name = self._editing_tool_preset_name or "(New Tool Preset)" - imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}"); imgui.separator() - - if imgui.begin_table("tp_meta", 2): - imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, self._editing_tool_preset_name = imgui.input_text("##etpn", self._editing_tool_preset_name) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column() - if imgui.radio_button("Global", self._editing_tool_preset_scope == "global"): self._editing_tool_preset_scope = "global" - imgui.same_line(); - if imgui.radio_button("Project", self._editing_tool_preset_scope == "project"): self._editing_tool_preset_scope = "project" - imgui.end_table() - - rem_y = imgui.get_content_region_avail().y - 80 - if self._tool_list_open and self._bias_list_open: h1, h2 = rem_y * self._tool_split_v, rem_y - (rem_y * self._tool_split_v) - 10 - elif self._tool_list_open: h1, h2 = rem_y, 0 - elif self._bias_list_open: h1, h2 = 0, rem_y - else: h1, h2 = 0, 0 - - imgui.dummy(imgui.ImVec2(0, 4)) - opened_t = imgui.collapsing_header("Categories & Tools", imgui.TreeNodeFlags_.default_open) - if opened_t != self._tool_list_open: self._tool_list_open = opened_t - if self._tool_list_open: - imgui.text("Filter:"); imgui.same_line() - cat_opts = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) - f_idx = cat_opts.index(self.ui_tool_filter_category) if self.ui_tool_filter_category in cat_opts else 0 - imgui.set_next_item_width(200); ch_cat, next_f_idx = imgui.combo("##tp_filter", f_idx, cat_opts) - if ch_cat: self.ui_tool_filter_category = cat_opts[next_f_idx] - imgui.begin_child("tp_scroll", imgui.ImVec2(0, h1), True) - if True: - for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): - if self.ui_tool_filter_category != "All" and self.ui_tool_filter_category != cat_name: continue - if imgui.tree_node(cat_name): - if cat_name not in self._editing_tool_preset_categories: self._editing_tool_preset_categories[cat_name] = [] - curr_cat_tools = self._editing_tool_preset_categories[cat_name] - if imgui.begin_table(f"tt_{cat_name}", 2, imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("Tool", imgui.TableColumnFlags_.width_fixed, 250); imgui.table_setup_column("Ctrls", imgui.TableColumnFlags_.width_stretch) - for tool_name in default_tools: - tool = next((t for t in curr_cat_tools if t.name == tool_name), None) - mode = "disabled" if tool is None else tool.approval - imgui.table_next_row(); imgui.table_next_column(); imgui.text(tool_name); imgui.table_next_column() - if imgui.radio_button(f"Off##{cat_name}_{tool_name}", mode == "disabled"): - if tool: curr_cat_tools.remove(tool) - imgui.same_line(); - if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"): - if not tool: tool = models.Tool(name=tool_name, approval="auto"); current_cat_tools.append(tool) - else: tool.approval = "auto" - imgui.same_line(); - if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"): - if not tool: tool = models.Tool(name=tool_name, approval="ask"); current_cat_tools.append(tool) - else: tool.approval = "ask" - imgui.end_table() - imgui.tree_pop() - imgui.end_child() - if self._bias_list_open: - imgui.button("###tool_splitter", imgui.ImVec2(-1, 4)) - if imgui.is_item_active(): self._tool_split_v = max(0.1, min(0.9, self._tool_split_v + imgui.get_io().mouse_delta.y / rem_y)) - - imgui.dummy(imgui.ImVec2(0, 4)) - opened_b = imgui.collapsing_header("Bias Profiles", imgui.TreeNodeFlags_.default_open) - if opened_b != self._bias_list_open: self._bias_list_open = opened_b - if self._bias_list_open: - imgui.begin_child("bias_area", imgui.ImVec2(0, h2), True) - if True: - if imgui.begin_table("bias_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("BList", imgui.TableColumnFlags_.width_fixed, 150); imgui.table_setup_column("BEdit", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row(); imgui.table_next_column() - imgui.begin_child("blist_pane", imgui.ImVec2(0, 0), False) - if True: - if imgui.button("New Profile", imgui.ImVec2(-1, 0)): - self._editing_bias_profile_name = ""; self._editing_bias_profile_tool_weights = {} - self._editing_bias_profile_category_multipliers = {}; self._selected_bias_profile_idx = -1 - imgui.separator(); bnames = sorted(self.bias_profiles.keys()) - for i, bname in enumerate(bnames): - if bname and imgui.selectable(f"{bname}##b_{i}", self._selected_bias_profile_idx == i)[0]: - self._selected_bias_profile_idx = i; self._editing_bias_profile_name = bname; prof = self.bias_profiles[bname] - self._editing_bias_profile_tool_weights = copy.deepcopy(prof.tool_weights); self._editing_bias_profile_category_multipliers = copy.deepcopy(prof.category_multipliers) - imgui.end_child() - - imgui.table_next_column() - imgui.begin_child("bedit_pane", imgui.ImVec2(0, 0), False) - if True: - imgui.text("Name:"); imgui.same_line(); imgui.set_next_item_width(-1); _, self._editing_bias_profile_name = imgui.input_text("##bname", self._editing_bias_profile_name) - rem_bias_y = imgui.get_content_region_avail().y - 45 - if self._bias_weights_open and self._bias_cats_open: bh1, bh2 = rem_bias_y * self._bias_split_v, rem_bias_y - (rem_bias_y * self._bias_split_v) - 10 - elif self._bias_weights_open: bh1, bh2 = rem_bias_y, 0 - elif self._bias_cats_open: bh1, bh2 = 0, rem_bias_y - else: bh1, bh2 = 0, 0 - - opened_bw = imgui.collapsing_header("Tool Weights", imgui.TreeNodeFlags_.default_open) - if opened_bw != self._bias_weights_open: self._bias_weights_open = opened_bw - if self._bias_weights_open: - imgui.begin_child("btool_scroll", imgui.ImVec2(0, bh1), True) - if True: - for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): - if imgui.tree_node(f"{cat_name}##b_list"): - if imgui.begin_table(f"bt_{cat_name}", 2): - imgui.table_setup_column("T", imgui.TableColumnFlags_.width_fixed, 220); imgui.table_setup_column("W", imgui.TableColumnFlags_.width_stretch) - for tn in default_tools: - imgui.table_next_row(); imgui.table_next_column(); imgui.text(tn); imgui.table_next_column() - curr_w = self._editing_bias_profile_tool_weights.get(tn, 3); imgui.set_next_item_width(-1) - ch_w, n_w = imgui.slider_int(f"##bw_{tn}", curr_w, 1, 10); - if ch_w: self._editing_bias_profile_tool_weights[tn] = n_w - imgui.end_table() - imgui.tree_pop() - imgui.end_child() - if self._bias_cats_open: - imgui.button("###bias_splitter", imgui.ImVec2(-1, 4)) - if imgui.is_item_active(): self._bias_split_v = max(0.1, min(0.9, self._bias_split_v + imgui.get_io().mouse_delta.y / rem_bias_y)) - - opened_bc = imgui.collapsing_header("Category Multipliers", imgui.TreeNodeFlags_.default_open) - if opened_bc != self._bias_cats_open: self._bias_cats_open = opened_bc - if self._bias_cats_open: - imgui.begin_child("bcat_scroll", imgui.ImVec2(0, bh2), True) - if True: - if imgui.begin_table("bcats", 2): - imgui.table_setup_column("C", imgui.TableColumnFlags_.width_fixed, 220); imgui.table_setup_column("M", imgui.TableColumnFlags_.width_stretch) - for cn in sorted(models.DEFAULT_TOOL_CATEGORIES.keys()): - imgui.table_next_row(); imgui.table_next_column(); imgui.text(cn); imgui.table_next_column() - curr_m = self._editing_bias_profile_category_multipliers.get(cn, 1.0); imgui.set_next_item_width(-1) - ch_m, n_m = imgui.slider_float(f"##cm_{cn}", curr_m, 0.1, 5.0, "%.1fx"); - if ch_m: self._editing_bias_profile_category_multipliers[cn] = n_m - imgui.end_table() - imgui.end_child() - - if imgui.button("Save Profile", imgui.ImVec2(-1, 0)): - try: - p = models.BiasProfile(name=self._editing_bias_profile_name, tool_weights=self._editing_bias_profile_tool_weights, category_multipliers=self._editing_bias_profile_category_multipliers) - self.controller._cb_save_bias_profile(p, self._editing_tool_preset_scope); self.ai_status = f"Saved: {p.name}" - except Exception as e: self.ai_status = f"Error: {e}" - imgui.end_child() - imgui.end_table() - imgui.end_child() - imgui.end_child() - - # --- Footer Buttons --- - imgui.separator() - if imgui.button("Save##tp", imgui.ImVec2(100, 0)): - if self._editing_tool_preset_name.strip(): self.controller._cb_save_tool_preset(self._editing_tool_preset_name.strip(), self._editing_tool_preset_categories, self._editing_tool_preset_scope); self.ai_status = f"Saved: {self._editing_tool_preset_name}" - imgui.same_line() - if imgui.button("Delete##tp", imgui.ImVec2(100, 0)): - if self._editing_tool_preset_name: self.controller._cb_delete_tool_preset(self._editing_tool_preset_name, self._editing_tool_preset_scope); self._editing_tool_preset_name = ""; self._selected_tool_preset_idx = -1 - imgui.same_line() - if not is_embedded: - if imgui.button("Close##tp", imgui.ImVec2(100, 0)): self.show_tool_preset_manager_window = False - imgui.end_table() + render_tool_preset_manager_content(self, is_embedded) def _render_tool_preset_manager_window(self, is_embedded: bool = False) -> None: - if not self.show_tool_preset_manager_window and not is_embedded: return - if not is_embedded: - imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) - with imscope.window("Tool Preset Manager", self.show_tool_preset_manager_window) as (opened, visible): - self.show_tool_preset_manager_window = visible - if opened: self._render_tool_preset_manager_content(is_embedded=is_embedded) - else: - self._render_preset_manager_content(is_embedded=is_embedded) + render_tool_preset_manager_window(self, is_embedded) def _render_persona_editor_window(self, is_embedded: bool = False) -> None: - if not self.show_persona_editor_window and not is_embedded: return - if not is_embedded: - imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) - opened, self.show_persona_editor_window = imgui.begin("Persona Editor", self.show_persona_editor_window) - if not opened: - imgui.end(); return - - if imgui.begin_table("persona_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - # --- Left Sidebar --- - imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row() - - imgui.table_next_column() - imgui.begin_child("persona_list_pane", imgui.ImVec2(0, 0), False) - if True: - if imgui.button("New Persona", imgui.ImVec2(-1, 0)): - self._editing_persona_name = ""; self._editing_persona_system_prompt = "" - self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = "" - self._editing_persona_context_preset_id = "" - self._editing_persona_aggregation_strategy = "" - self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}] - self._editing_persona_scope = "project"; self._editing_persona_is_new = True - imgui.separator() - personas = getattr(self.controller, 'personas', {}) - for name in sorted(personas.keys()): - if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]: - p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or "" - self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or "" - self._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or "" - self._editing_persona_aggregation_strategy = getattr(p, 'aggregation_strategy', '') or "" - import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else [] - self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False - imgui.end_child() - - # --- Right Editor --- - imgui.table_next_column() - avail = imgui.get_content_region_avail() - imgui.begin_child("persona_editor_content", imgui.ImVec2(0, avail.y - 45), False) - if True: - header_text = "New Persona" if getattr(self, '_editing_persona_is_new', True) else f"Editing Persona: {self._editing_persona_name}" - imgui.text_colored(C_IN, header_text); imgui.separator() - - if imgui.begin_table("p_meta", 2): - imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 60); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, self._editing_persona_name = imgui.input_text("##pname", self._editing_persona_name, 128) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column() - if imgui.radio_button("Global##pscope", getattr(self, '_editing_persona_scope', 'project') == "global"): self._editing_persona_scope = "global" - imgui.same_line(); - if imgui.radio_button("Project##pscope", getattr(self, '_editing_persona_scope', 'project') == "project"): self._editing_persona_scope = "project" - imgui.end_table() - - rem_y = imgui.get_content_region_avail().y - 100 - if self._persona_models_open and self._persona_prompt_open: h1, h2 = rem_y * self._persona_split_v, rem_y - (rem_y * self._persona_split_v) - 10 - elif self._persona_models_open: h1, h2 = rem_y, 0 - elif self._persona_prompt_open: h1, h2 = 0, rem_y - else: h1, h2 = 0, 0 - - imgui.dummy(imgui.ImVec2(0, 4)) - opened_models = imgui.collapsing_header("Preferred Models", imgui.TreeNodeFlags_.default_open) - if opened_models != self._persona_models_open: self._persona_models_open = opened_models - - if self._persona_models_open: - imgui.begin_child("pref_models_scroll", imgui.ImVec2(0, h1), True) - if True: - to_remove = [] - providers = models.PROVIDERS - if not hasattr(self, '_persona_pref_models_expanded'): self._persona_pref_models_expanded = {} - for i, entry in enumerate(self._editing_persona_preferred_models_list): - imgui.push_id(f"pref_model_{i}") - prov, mod, is_expanded = entry.get("provider", "Unknown"), entry.get("model", "Unknown"), self._persona_pref_models_expanded.get(i, False) - if imgui.button("-" if is_expanded else "+"): self._persona_pref_models_expanded[i] = not is_expanded - imgui.same_line(); imgui.text(f"{i+1}."); imgui.same_line(); imgui.text_colored(C_LBL, f"{prov}"); imgui.same_line(); imgui.text("-"); imgui.same_line(); imgui.text_colored(C_IN, f"{mod}") - if not is_expanded: - imgui.same_line(); summary = f" (T:{entry.get('temperature', 0.7):.1f}, P:{entry.get('top_p', 1.0):.2f}, M:{entry.get('max_output_tokens', 0)})" - imgui.text_colored(C_SUB, summary) - imgui.same_line(imgui.get_content_region_avail().x - 30); - if imgui.button("x"): to_remove.append(i) - if is_expanded: - imgui.indent(20) - if imgui.begin_table("model_settings", 2, imgui.TableFlags_.borders_inner_v): - imgui.table_setup_column("Label", imgui.TableColumnFlags_.width_fixed, 120); imgui.table_setup_column("Control", imgui.TableColumnFlags_.width_stretch) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Provider:"); imgui.table_next_column(); imgui.set_next_item_width(-1) - p_idx = providers.index(prov) + 1 if prov in providers else 0; ch_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers) - if ch_p: entry["provider"] = providers[p_idx-1] if p_idx > 0 else "" - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Model:"); imgui.table_next_column(); imgui.set_next_item_width(-1) - m_list = self.controller.all_available_models.get(entry.get("provider", ""), []); m_idx = m_list.index(mod) + 1 if mod in m_list else 0 - ch_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list) - if ch_m: entry["model"] = m_list[m_idx-1] if m_idx > 0 else "" - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Temperature:"); imgui.table_next_column(); cw = imgui.get_content_region_avail().x - imgui.set_next_item_width(cw * 0.7); _, entry["temperature"] = imgui.slider_float("##ts", entry.get("temperature", 0.7), 0.0, 2.0, "%.1f") - imgui.same_line(); imgui.set_next_item_width(-1); _, entry["temperature"] = imgui.input_float("##ti", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f") - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Top-P:"); imgui.table_next_column() - imgui.set_next_item_width(cw * 0.7); _, entry["top_p"] = imgui.slider_float("##tp_s", entry.get("top_p", 1.0), 0.0, 1.0, "%.2f") - imgui.same_line(); imgui.set_next_item_width(-1); _, entry["top_p"] = imgui.input_float("##tp_i", entry.get("top_p", 1.0), 0.05, 0.05, "%.2f") - imgui.table_next_row(); imgui.table_next_column(); imgui.text("Max Tokens:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096)) - imgui.table_next_row(); imgui.table_next_column(); imgui.text("History Limit:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["history_trunc_limit"] = imgui.input_int("##hist", entry.get("history_trunc_limit", 900000)) - imgui.end_table() - imgui.unindent(20) - imgui.pop_id() - for i in reversed(to_remove): self._editing_persona_preferred_models_list.pop(i) - imgui.end_child() - if self._persona_prompt_open: - imgui.button("###persona_splitter", imgui.ImVec2(-1, 4)) - if imgui.is_item_active(): self._persona_split_v = max(0.1, min(0.9, self._persona_split_v + imgui.get_io().mouse_delta.y / rem_y)) - - imgui.dummy(imgui.ImVec2(0, 2)) - if imgui.button("Add Preferred Model", imgui.ImVec2(-1, 0)): self._editing_persona_preferred_models_list.append({"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}) - - imgui.dummy(imgui.ImVec2(0, 2)) - if imgui.begin_table("p_assign", 2): - imgui.table_setup_column("C1"); imgui.table_setup_column("C2"); imgui.table_next_row() - imgui.table_next_column(); imgui.text("Tool Preset:"); tn = ["None"] + sorted(self.controller.tool_presets.keys()) - t_idx = tn.index(self._editing_persona_tool_preset_id) if getattr(self, '_editing_persona_tool_preset_id', '') in tn else 0 - imgui.set_next_item_width(-1); _, t_idx = imgui.combo("##ptp", t_idx, tn); self._editing_persona_tool_preset_id = tn[t_idx] if t_idx > 0 else "" - imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys()) - b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0 - imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else "" - imgui.table_next_row() - imgui.table_next_column(); imgui.text("Context Preset:"); cn = ["None"] + sorted(self.controller.project.get("context_presets", {}).keys()) - c_idx = cn.index(self._editing_persona_context_preset_id) if getattr(self, '_editing_persona_context_preset_id', '') in cn else 0 - imgui.set_next_item_width(-1); _, c_idx = imgui.combo("##pcp", c_idx, cn); self._editing_persona_context_preset_id = cn[c_idx] if c_idx > 0 else "" - imgui.table_next_column(); imgui.text("Aggregation Strategy:"); sn = ["auto", "full", "summarize", "skeleton"] - s_idx = sn.index(self._editing_persona_aggregation_strategy) if getattr(self, '_editing_persona_aggregation_strategy', '') in sn else 0 - imgui.set_next_item_width(-1); _, s_idx = imgui.combo("##pas", s_idx, sn); self._editing_persona_aggregation_strategy = sn[s_idx] - imgui.end_table() - - if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True - - imgui.dummy(imgui.ImVec2(0, 4)); imgui.separator() - opened_prompt = imgui.collapsing_header("System Prompt", imgui.TreeNodeFlags_.default_open) - if opened_prompt != self._persona_prompt_open: self._persona_prompt_open = opened_prompt - - if self._persona_prompt_open: - imgui.begin_child("p_prompt_header_pane", imgui.ImVec2(0, 30), False) - if True: - imgui.text("Template:"); imgui.same_line(); p_pre = ["Select..."] + sorted(self.controller.presets.keys()) - if not hasattr(self, "_load_preset_idx"): self._load_preset_idx = 0 - imgui.set_next_item_width(200); _, self._load_preset_idx = imgui.combo("##load_p", self._load_preset_idx, p_pre) - imgui.same_line(); - if imgui.button("Apply"): - if self._load_preset_idx > 0: self._editing_persona_system_prompt = self.controller.presets[p_pre[self._load_preset_idx]].system_prompt - imgui.same_line(); - if imgui.button("Manage"): self.show_preset_manager_window = True - imgui.end_child() - _, self._editing_persona_system_prompt = imgui.input_text_multiline("##pprompt", self._editing_persona_system_prompt, imgui.ImVec2(-1, h2)) - imgui.end_child() - - # --- Footer Buttons --- - imgui.separator() - if imgui.button("Save##pers", imgui.ImVec2(100, 0)): - if self._editing_persona_name.strip(): - try: - import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, context_preset=self._editing_persona_context_preset_id or None, aggregation_strategy=self._editing_persona_aggregation_strategy or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list)) - self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}" - except Exception as e: self.ai_status = f"Error: {e}" - else: self.ai_status = "Name required" - imgui.same_line(); - if imgui.button("Delete##pers", imgui.ImVec2(100, 0)): - if not getattr(self, '_editing_persona_is_new', True) and self._editing_persona_name: - self.controller._cb_delete_persona(self._editing_persona_name, getattr(self, '_editing_persona_scope', 'project')) - self._editing_persona_name = ""; self._editing_persona_is_new = True - if not is_embedded: - imgui.same_line() - if imgui.button("Close##pers", imgui.ImVec2(100, 0)): - self.show_persona_editor_window = False - imgui.end_table() - - if not is_embedded: - imgui.end() + render_persona_editor_window(self, is_embedded) def _render_provider_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel") - imgui.text("Provider") - if imgui.begin_combo("##prov", self.current_provider): - for p in models.PROVIDERS: - if imgui.selectable(p, p == self.current_provider)[0]: - self.current_provider = p - imgui.end_combo() - imgui.separator() - imgui.text("Model") - if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): - for m in self.available_models: - if imgui.selectable(m, m == self.current_model)[0]: - self.current_model = m - imgui.end_list_box() - imgui.separator() - imgui.text("Parameters") - # Temperature - imgui.push_id("temp") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, self.temperature = imgui.slider_float("##slider", self.temperature, 0.0, 2.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.temperature = imgui.input_float("Temp", self.temperature, 0.0, 0.0, "%.2f") - imgui.pop_id() - - # Top-P - imgui.push_id("top_p") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, self.top_p = imgui.slider_float("##slider", self.top_p, 0.0, 1.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.top_p = imgui.input_float("Top-P", self.top_p, 0.0, 0.0, "%.2f") - imgui.pop_id() - - # Max Tokens - imgui.push_id("max_tokens") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, self.max_tokens = imgui.slider_int("##slider", self.max_tokens, 1, 32768) - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.max_tokens = imgui.input_int("MaxTok", self.max_tokens) - imgui.pop_id() - - ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) - - if self.current_provider == "gemini_cli": - imgui.separator() - imgui.text("Gemini CLI") - sid = "None" - if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None" - imgui.text("Session ID:"); imgui.same_line(); self._render_selectable_label("gemini_cli_sid", sid, width=200) - if imgui.button("Reset CLI Session"): ai_client.reset_session() - imgui.text("Binary Path") - ch, self.ui_gemini_cli_path = imgui.input_text("##gcli_path", self.ui_gemini_cli_path) - imgui.same_line() - if imgui.button("Browse##gcli"): - r = hide_tk_root() - p = filedialog.askopenfilename(title="Select gemini CLI binary") - r.destroy() - if p: self.ui_gemini_cli_path = p - if ch: - if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: - ai_client._gemini_cli_adapter.binary_path = self.ui_gemini_cli_path - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_provider_panel") + render_provider_panel(self) def _render_persona_selector_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel") - imgui.text("Persona") - if not hasattr(self, 'ui_active_persona'): self.ui_active_persona = "" - personas = getattr(self.controller, 'personas', {}) - if imgui.begin_combo("##persona", self.ui_active_persona or "None"): - if imgui.selectable("None", not self.ui_active_persona)[0]: self.ui_active_persona = "" - for pname in sorted(personas.keys()): - if not pname: continue - if imgui.selectable(pname, pname == self.ui_active_persona)[0]: - self.ui_active_persona = pname - if pname in personas: - persona = personas[pname] - self._editing_persona_name = persona.name - self._editing_persona_system_prompt = persona.system_prompt or "" - self._editing_persona_tool_preset_id = persona.tool_preset or "" - self._editing_persona_bias_profile_id = persona.bias_profile or "" - self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - self._editing_persona_is_new = False - - # Apply persona to current state immediately - if persona.preferred_models and len(persona.preferred_models) > 0: - first_model = persona.preferred_models[0] - if first_model.get("provider"): - self.current_provider = first_model.get("provider") - if first_model.get("model"): - self.current_model = first_model.get("model") - if first_model.get("temperature") is not None: - ai_client.temperature = first_model.get("temperature") - self.temperature = first_model.get("temperature") - if first_model.get("max_output_tokens"): - ai_client.max_output_tokens = first_model.get("max_output_tokens") - self.max_tokens = first_model.get("max_output_tokens") - if first_model.get("history_trunc_limit"): - self.history_trunc_limit = first_model.get("history_trunc_limit") - - if persona.system_prompt: self.ui_project_system_prompt = persona.system_prompt - if persona.tool_preset: - self.ui_active_tool_preset = persona.tool_preset - ai_client.set_tool_preset(persona.tool_preset) - if persona.bias_profile: - self.ui_active_bias_profile = persona.bias_profile - ai_client.set_bias_profile(persona.bias_profile) - if getattr(persona, 'context_preset', None): - self.ui_active_context_preset = persona.context_preset - self.load_context_preset(persona.context_preset) - imgui.end_combo() - imgui.same_line() - if imgui.button("Manage Personas"): - self.show_persona_editor_window = True - if self.ui_active_persona and self.ui_active_persona in personas: - persona = personas[self.ui_active_persona] - self._editing_persona_name = persona.name - self._editing_persona_system_prompt = persona.system_prompt or "" - self._editing_persona_tool_preset_id = persona.tool_preset or "" - self._editing_persona_bias_profile_id = persona.bias_profile or "" - self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name) - self._editing_persona_is_new = False - else: - self._editing_persona_name = "" - self._editing_persona_system_prompt = "" - self._editing_persona_tool_preset_id = "" - self._editing_persona_bias_profile_id = "" - self._editing_persona_context_preset_id = "" - self._editing_persona_aggregation_strategy = "" - self._editing_persona_preferred_models_list = [{ - "provider": self.current_provider, - "model": self.current_model, - "temperature": getattr(self, "temperature", 0.7), - "max_output_tokens": getattr(self, "max_tokens", 4096), - "history_trunc_limit": getattr(self, "history_trunc_limit", 900000) - }] - self._editing_persona_scope = "project" - self._editing_persona_is_new = True - imgui.separator() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_persona_selector_panel") + render_persona_selector_panel(self) #endregion: AI Settings #region: Context Management def _render_files_and_media(self) -> None: - """ - [C: tests/test_gui_fast_render.py:test_render_files_and_media_fast] - """ - 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", -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", -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 + render_files_and_media(self) 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") + render_files_panel(self, height_override) 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.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() - 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 - 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}"): - self.screenshots.pop(i) - break - imgui.same_line(); imgui.text(s) - imgui.end_child() - if imgui.button("Add Screenshot(s)"): - 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: - if p not in self.screenshots: self.screenshots.append(p) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel") + render_screenshots_panel(self, height_override) def _render_context_composition_panel(self) -> None: - """ - [C: tests/test_auto_slices.py:test_add_all_triggers_auto_slices, tests/test_gui_fast_render.py:test_render_context_composition_panel_fast] - """ - if imgui.collapsing_header("Context Composition##panel"): - total_lines, total_ast = self._update_context_file_stats() - self._render_context_batch_actions(total_lines, total_ast) - self._render_context_files_table() - - imgui.separator() - if imgui.collapsing_header("Screenshots"): - self._render_context_screenshots() - imgui.separator() - self._render_context_presets() + render_context_composition_panel(self) def _render_ast_inspector_modal(self) -> None: - """ - [C: tests/test_ast_inspector_extended.py:test_ast_inspector_line_range_parsing] - """ - 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) --- - imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) - if 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) --- - imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) - if 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 + render_ast_inspector_modal(self) def _render_add_context_files_modal(self) -> None: - """ - [C: tests/test_auto_slices.py:test_add_selected_triggers_auto_slices] - """ - if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text("Select files from project to add to context:") - imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) - if True: - # Create a temporary selection set if not initialized - if not hasattr(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() + render_add_context_files_modal(self) 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() + render_save_workspace_profile_modal(self) def _render_context_presets_panel(self) -> None: - imgui.text_colored(C_IN, "Context Presets") - imgui.separator() - changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name) - if changed: self.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save Current"): - if self.ui_new_context_preset_name.strip(): - self.save_context_preset(self.ui_new_context_preset_name.strip()) - - imgui.separator() - presets = self.controller.project.get('context_presets', {}) - for name in sorted(presets.keys()): - preset = presets[name] - n_files = len(preset.get('files', [])) - n_shots = len(preset.get('screenshots', [])) - imgui.text(f"{name} ({n_files} files, {n_shots} shots)") - imgui.same_line() - if imgui.button(f"Load##{name}"): self.load_context_preset(name) - imgui.same_line() - if imgui.button(f"Delete##{name}"): self.delete_context_preset(name) + render_context_presets_panel(self) def _populate_auto_slices(self, f_item: models.FileItem) -> None: """ @@ -3045,176 +1056,16 @@ class App: f_item.custom_slices.append(slice_data) def _render_context_screenshots(self) -> None: - for i, s in enumerate(self.screenshots): imgui.text(s) + render_context_screenshots(self) def _render_context_batch_actions(self, total_lines: int, total_ast: int) -> None: - imgui.text("Batch:") - for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: - if imgui.button(f"{mode.capitalize()}##batch"): - for f in self.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: f.view_mode = mode - imgui.same_line() - if imgui.button("Sel All##selall"): - for f in self.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - self.ui_selected_context_files.add(f_path) - imgui.same_line() - if imgui.button("Unsel All##unselall"): self.ui_selected_context_files.clear() - imgui.same_line() - if imgui.button("Add Files"): imgui.open_popup("Select Context Files") - imgui.same_line() - if imgui.button("Add All##addall"): - context_paths = {f.path if hasattr(f, "path") else str(f) for f in self.context_files} - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path not in context_paths: - f_copy = copy.deepcopy(f) - self.context_files.append(f_copy) - self._populate_auto_slices(f_copy) - imgui.same_line() - if imgui.button("Del##batch"): - new_files = [] - for f in self.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path not in self.ui_selected_context_files: new_files.append(f) - self.context_files = new_files - self.ui_selected_context_files.clear() - imgui.same_line() - imgui.text(f" | Total: {len(self.context_files)} files, {total_lines} lines, {total_ast} AST elements") + render_context_batch_actions(self, total_lines, total_ast) def _render_context_files_table(self) -> None: - imgui.dummy(imgui.ImVec2(0, 4)) - grouped_files = aggregate.group_files_by_dir(self.context_files) - - with imscope.table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders) as active: - if active: - imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_headers_row() - - file_indices = {id(f): idx for idx, f in enumerate(self.context_files)} - - for dir_name, g_files in grouped_files.items(): - imgui.table_next_row() - imgui.table_set_column_index(0) - with imscope.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open) as is_open: - imgui.table_set_column_index(1) - if is_open: - for f_item in g_files: - i = file_indices[id(f_item)] - imgui.table_next_row() - imgui.table_set_column_index(0) - - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - is_sel = f_path in self.ui_selected_context_files - changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) - if changed_sel: - if imgui.get_io().key_shift and self._last_selected_context_index != -1: - start = min(self._last_selected_context_index, i) - end = max(self._last_selected_context_index, i) - for idx in range(start, end + 1): - item = self.context_files[idx] - item_path = item.path if hasattr(item, "path") else str(item) - if is_sel: self.ui_selected_context_files.add(item_path) - else: self.ui_selected_context_files.discard(item_path) - else: - if is_sel: self.ui_selected_context_files.add(f_path) - else: self.ui_selected_context_files.discard(f_path) - self._last_selected_context_index = i - imgui.same_line() - - mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 - cache_key = f"{f_path}_{mtime}" - stats = self._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0}) - f_name = os.path.basename(f_path) - imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})") - - if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): - imgui.same_line() - if imgui.button(f"[Inspect]##{i}"): - self.ui_inspecting_ast_file = f_item - self._show_ast_inspector = True - - imgui.same_line() - if imgui.button(f"[Slices]##{i}"): - self.ui_editing_slices_file = f_item - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - self.text_viewer_title = f"Slices: {f_path}" - try: - self.text_viewer_content = mcp_client.read_file(f_path) - except Exception as e: - self.text_viewer_content = f"Error reading file: {e}" - self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' - self.show_text_viewer = True - - imgui.table_set_column_index(1) - if not hasattr(f_item, "view_mode"): f_item.view_mode = "summary" - view_modes = ["full", "summary", "skeleton", "outline", "masked", "none"] - try: - current_idx = view_modes.index(f_item.view_mode) - except ValueError: - current_idx = 1 - f_item.view_mode = "summary" - imgui.set_next_item_width(120) - changed_vm, new_idx = imgui.combo(f"##vm{i}", current_idx, view_modes) - if changed_vm: f_item.view_mode = view_modes[new_idx] - - imgui.same_line() - if imgui.button(f"[Save]##vpsave{i}"): imgui.open_popup(f"save_vp_popup{i}") - - if imgui.begin_popup(f"save_vp_popup{i}"): - imgui.text("Preset Name:") - changed_pname, self.ui_new_vp_name = imgui.input_text(f"##pname{i}", self.ui_new_vp_name) - if imgui.button("OK"): - if self.ui_new_vp_name.strip(): - self.controller._cb_save_view_preset(self.ui_new_vp_name.strip(), f_item) - self.ui_new_vp_name = "" - imgui.close_current_popup() - imgui.end_popup() - - imgui.same_line() - if imgui.button(f"[Load]##vpload{i}"): imgui.open_popup(f"load_vp_popup{i}") - - if imgui.begin_popup(f"load_vp_popup{i}"): - vp_names = sorted([vp.name for vp in self.controller.view_presets]) - if not vp_names: imgui.text("No presets saved.") - for vp_name in vp_names: - if imgui.selectable(vp_name): - self.controller._cb_apply_view_preset(vp_name, f_item) - imgui.close_current_popup() - imgui.end_popup() - if hasattr(f_item, "custom_slices") and f_item.custom_slices: - imgui.same_line() - imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") + render_context_files_table(self) def _render_context_presets(self) -> None: - imgui.text("Presets") - presets = self.controller.project.get('context_presets', {}) - preset_names = [""] + sorted(presets.keys()) - active = getattr(self, "ui_active_context_preset", "") - if active not in preset_names: active = "" - try: - idx = preset_names.index(active) - except ValueError: - idx = 0 - ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) - if ch: - self.ui_active_context_preset = preset_names[new_idx] - if preset_names[new_idx]: self.load_context_preset(preset_names[new_idx]) - imgui.same_line() - changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", "")) - if changed: self.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save##ctx"): - if getattr(self, "ui_new_context_preset_name", "").strip(): - self.save_context_preset(self.ui_new_context_preset_name.strip()) - self.ui_new_context_preset_name = "" - imgui.same_line() - if imgui.button("Delete##ctx"): - if getattr(self, "ui_active_context_preset", ""): - self.delete_context_preset(self.ui_active_context_preset) - self.ui_active_context_preset = "" + render_context_presets(self) def _update_context_file_stats(self) -> tuple[int, int]: if not hasattr(self, '_file_stats_cache'): self._file_stats_cache = {} @@ -3251,896 +1102,78 @@ class App: #region: Discussions def _render_discussion_hub(self) -> None: - with imscope.tab_bar("discussion_hub_tabs"): - with imscope.tab_item("Discussion") as (exp, opened): - if exp: self._render_discussion_tab() - with imscope.tab_item("Context Composition") as (exp, opened): - if exp: self._render_context_composition_panel() - with imscope.tab_item("Snapshot") as (exp, opened): - if exp: self._render_snapshot_tab() - with imscope.tab_item("Takes") as (exp, opened): - if exp: self._render_takes_panel() - return + render_discussion_hub(self) 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 + render_discussion_entries(self) 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() + render_discussion_entry(self, entry, index) 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" + render_discussion_entry_controls(self) 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() + render_discussion_entry_read_mode(self, entry, index) 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) + render_discussion_metadata(self) 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_fast_render.py:test_render_discussion_panel_fast, tests/test_gui_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup] - """ - if 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") - return + render_discussion_panel(self) 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 = "" - return + render_discussion_roles(self) 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() - return + render_discussion_selector(self) 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.") + render_discussion_tab(self) def _render_takes_panel(self) -> None: - imgui.text("Takes & Synthesis") - imgui.separator() - discussions = self.project.get('discussion', {}).get('discussions', {}) - if not hasattr(self, 'ui_synthesis_selected_takes'): - self.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(self, 'ui_synthesis_prompt'): - self.ui_synthesis_prompt = "" - if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_headers_row() - for name, disc in list(discussions.items()): - imgui.table_next_row() - imgui.table_set_column_index(0) - is_active = name == self.active_discussion - if is_active: - imgui.text_colored(C_IN, name) - else: - imgui.text(name) - imgui.table_set_column_index(1) - history = disc.get('history', []) - imgui.text(f"{len(history)}") - imgui.table_set_column_index(2) - if imgui.button(f"Switch##{name}"): - self._switch_discussion(name) - imgui.same_line() - if name != "main" and imgui.button(f"Delete##{name}"): - del discussions[name] - imgui.end_table() - imgui.separator() - imgui.text("Synthesis") - imgui.text("Select takes to synthesize:") - for name in discussions: - _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - from src import synthesis_formatter - takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" - new_name = "synthesis_take" - counter = 1 - while new_name in discussions: - new_name = f"synthesis_take_{counter}" - counter += 1 - self._create_discussion(new_name) - with self._disc_entries_lock: - self.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - self._handle_generate_send() + render_takes_panel(self) 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() + render_prior_session_view(self) 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() + render_thinking_indicator(self) def _render_message_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_message_panel") - # LIVE indicator - is_live = self.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] - if is_live: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - c = imgui.ImVec4(0.39, 1.0, 0.39, alpha) - if theme.is_nerv_active(): c = vec4(80, 255, 80, alpha) # DATA_GREEN for LIVE in NERV - imgui.text_colored(c, "LIVE") - imgui.separator() - ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40)) - # Keyboard shortcuts - io = imgui.get_io() - ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) - if ctrl_l: self.ui_ai_input = "" - imgui.separator() - is_busy = self.ai_status in ['sending...', 'streaming...'] - send_busy = False - with self._send_thread_lock: - if self.send_thread and self.send_thread.is_alive(): send_busy = True - if is_busy: send_busy = True - - imgui.begin_disabled(send_busy) - ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) - label = "Gen + Send (Busy)" if send_busy else "Gen + Send" - if (imgui.button(label) or ctrl_enter) and not send_busy: self._handle_generate_send() - imgui.end_disabled() - imgui.same_line() - if imgui.button("MD Only"): self._handle_md_only() - imgui.same_line() - if imgui.button("Inject File"): self.show_inject_modal = True - imgui.same_line() - if imgui.button("-> History"): - if self.ui_ai_input: self.disc_entries.append({"role": "User", "content": self.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("Reset"): self._handle_reset_session() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_message_panel") + render_message_panel(self) def _render_synthesis_panel(self) -> None: - """ - - Renders a panel for synthesizing multiple discussion takes. - [C: tests/test_gui_synthesis.py:test_render_synthesis_panel] - """ - imgui.text("Select takes to synthesize:") - discussions = self.project.get('discussion', {}).get('discussions', {}) - if not hasattr(self, 'ui_synthesis_selected_takes'): self.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(self, 'ui_synthesis_prompt'): self.ui_synthesis_prompt = "" - for name in discussions: _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - 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}" - - new_name = "synthesis_take" - 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()}) - self._handle_generate_send() + render_synthesis_panel(self) def _render_snapshot_tab(self) -> None: - if imgui.begin_tab_bar("snapshot_tabs"): - if imgui.begin_tab_item("Aggregate MD")[0]: - display_md = self.last_aggregate_markdown - 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: - persona = self.controller.personas.get(persona_name) - if persona and persona.context_preset: - cp_name = persona.context_preset - if cp_name in self._focus_md_cache: - display_md = self._focus_md_cache[cp_name] - else: - flat = src.project_manager.flat_config(self.controller.project, self.active_discussion) - cp = self.controller.project.get('context_presets', {}).get(cp_name) - if cp: - flat["files"]["paths"] = cp.get("files", []) - flat["screenshots"]["paths"] = cp.get("screenshots", []) - full_md, _, _ = src.aggregate.run(flat) - self._focus_md_cache[cp_name] = full_md - display_md = full_md - if imgui.button("Copy"): imgui.set_clipboard_text(display_md) - imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True) - markdown_helper.render(display_md, context_id="snapshot_agg") - imgui.end_child() - imgui.end_tab_item() - if imgui.begin_tab_item("System Prompt")[0]: - if imgui.button("Copy"): imgui.set_clipboard_text(self.last_resolved_system_prompt) - imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) - markdown_helper.render(self.last_resolved_system_prompt, context_id="snapshot_sys") - imgui.end_child() - imgui.end_tab_item() - imgui.end_tab_bar() + render_snapshot_tab(self) def _render_response_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_response_panel") - if self._trigger_blink: - self._trigger_blink = False - self._is_blinking = True - self._blink_start_time = time.time() - try: - imgui.set_window_focus("Response") # type: ignore[call-arg] - except: - pass - is_blinking = False - blink_color = vec4(0, 0, 0, 0) - if self._is_blinking: - elapsed = time.time() - self._blink_start_time - if elapsed > 1.5: - self._is_blinking = False - else: - is_blinking = True - val = math.sin(elapsed * 8 * math.pi) - alpha = 50/255 if val > 0 else 0 - blink_color = vec4(0, 255, 0, alpha) - - with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): - with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): - with imscope.child("response_scroll_area", 0, -40, True): - with theme.ai_text_style(): - segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response) - if segments: - self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999) - markdown_helper.render(parsed_response, context_id="response") - - imgui.separator() - if imgui.button("-> History"): - if self.ai_response: - segments, response = thinking_parser.parse_thinking_trace(self.ai_response) - entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} - if segments: entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] - self.disc_entries.append(entry) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel") + render_response_panel(self) #endregion: Discussions #region: Operations Monitor 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] + render_operations_hub(self) def _render_tool_calls_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_calls_panel") - imgui.text("Tool call history") - imgui.same_line() - if imgui.button("Clear##tc"): - self._tool_log.clear() - self._tool_log_dirty = True - imgui.separator() - - log_to_render = self._tool_log_cache - flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y - - if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): - imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) - imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) - - imgui.table_headers_row() - - clipper = imgui.ListClipper() - clipper.begin(len(log_to_render)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - entry = log_to_render[i] - imgui.table_next_row() - - imgui.table_next_column() - imgui.text_colored(C_LBL, f"#{i+1}") - - imgui.table_next_column() - imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") - - imgui.table_next_column() - script = entry.get("script", "") - res = entry.get("result", "") - # Use a clear, formatted combined view for the detail window - combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" - - script_preview = script.replace("\n", " ")[:150] - if len(script) > 150: script_preview += "..." - self._render_selectable_label(f'tc_script_{i}', script_preview, width=-1) - if imgui.is_item_clicked(): - self.text_viewer_title = f"Tool Call #{i+1} Details" - self.text_viewer_content = combined - self.text_viewer_type = 'markdown' - self.show_text_viewer = True - - imgui.table_next_column() - res_preview = res.replace("\n", " ")[:30] - if len(res) > 30: res_preview += "..." - self._render_selectable_label(f'tc_res_{i}', res_preview, width=-1) - if imgui.is_item_clicked(): - self.text_viewer_title = f"Tool Call #{i+1} Details" - self.text_viewer_content = combined - self.text_viewer_type = 'markdown' - self.show_text_viewer = True - - imgui.end_table() - - if self._scroll_tool_calls_to_bottom: - imgui.set_scroll_here_y(1.0) - self._scroll_tool_calls_to_bottom = False - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel") + render_tool_calls_panel(self) def _render_comms_history_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_comms_history_panel") - st_col = vec4(200, 220, 160) - if theme.is_nerv_active(): st_col = vec4(80, 255, 80) # DATA_GREEN for status in NERV - imgui.text_colored(st_col, f"Status: {self.ai_status}") - imgui.same_line() - if imgui.button("Clear##comms"): - ai_client.clear_comms_log() - self._comms_log.clear() - self._comms_log_dirty = True - if self.is_viewing_prior_session: - imgui.same_line() - if imgui.button("Exit Prior Session"): - self.controller.cb_exit_prior_session() - 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_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)) - - imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) - - log_to_render = self._comms_log_cache - - clipper = imgui.ListClipper() - clipper.begin(len(log_to_render)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - entry = log_to_render[i] - imgui.push_id(f"comms_entry_{i}") - - i_display = i + 1 - ts = entry.get("ts", "00:00:00") - direction = entry.get("direction", "??") - kind = entry.get("kind", entry.get("type", "??")) - provider = entry.get("provider", "?") - model = entry.get("model", "?") - tier = entry.get("source_tier", "main") - payload = entry.get("payload", {}) - if not payload and kind not in ("request", "response", "tool_call", "tool_result"): - payload = entry # legacy - - # Row 1: #Idx TS DIR KIND Provider/Model [Tier] - imgui.text_colored(C_LBL, f"#{i_display}"); imgui.same_line() - imgui.text_colored(vec4(160, 160, 160), ts) - - latency = entry.get("latency") or entry.get("metadata", {}).get("latency") - if latency: - imgui.same_line() - imgui.text_colored(C_SUB, f" ({latency:.2f}s)") - - ticket_id = entry.get("mma_ticket_id") - if ticket_id: - imgui.same_line() - imgui.text_colored(vec4(255, 120, 120), f"[{ticket_id}]") - imgui.same_line() - d_col = DIR_COLORS.get(direction, C_VAL) - imgui.text_colored(d_col, direction); imgui.same_line() - k_col = KIND_COLORS.get(kind, C_VAL) - imgui.text_colored(k_col, kind); imgui.same_line() - imgui.text_colored(C_LBL, f"{provider}/{model}"); imgui.same_line() - imgui.text_colored(C_SUB, f"[{tier}]") - - # Optimized content rendering using _render_heavy_text logic - idx_str = str(i) - if kind == "request": - usage = payload.get("usage", {}) - if usage: - inp = usage.get("input_tokens", 0) - imgui.text_colored(C_LBL, f" tokens in:{inp}") - self._render_heavy_text("message", payload.get("message", ""), idx_str) - 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", {}) - usage_str = "" - if usage: - inp = usage.get("input_tokens", 0) - out = usage.get("output_tokens", 0) - cache = usage.get("cache_read_input_tokens", 0) - usage_str = f" in:{inp} out:{out}" - if cache: usage_str += f" cache:{cache}" - imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}") - - text_content = payload.get("text", "") - segments, parsed_response = thinking_parser.parse_thinking_trace(text_content) - if segments: 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) - - imgui.separator() - imgui.pop_id() - - if self._scroll_comms_to_bottom: - imgui.set_scroll_here_y(1.0) - self._scroll_comms_to_bottom = False - - imgui.end_child() - if self.is_viewing_prior_session: imgui.pop_style_color() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_comms_history_panel") + render_comms_history_panel(self) #endregion: Operations Monitor #region: Misc Tools 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 + render_text_viewer_window(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() + render_base_prompt_diff_modal(self) def _close_vscode_diff(self) -> None: if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: @@ -4151,48 +1184,7 @@ class App: self._vscode_diff_process = None def _render_patch_modal(self) -> None: - if not self._show_patch_modal: - return - imgui.open_popup("Apply Patch?") - with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - 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() + render_patch_modal(self) def _apply_pending_patch(self) -> None: if not self._pending_patch_text: @@ -4239,120 +1231,17 @@ class App: self._patch_error_message = str(e) def _render_external_editor_panel(self) -> None: - from src.external_editor import get_default_launcher - imgui.text("External Editor for Diff Viewing") - imgui.separator() - try: - launcher = get_default_launcher() - editors = launcher.config.editors - default_name = launcher.config.default_editor - if not editors: - imgui.text_colored(C_REQ, " No editors configured") - imgui.text("") - imgui.text("Add editors in config.toml:") - imgui.text(" [tools.text_editors.vscode]") - imgui.text(' path = "C:\\\\path\\\\to\\\\code.exe"') - imgui.text(' diff_args = ["--diff"]') - imgui.text("") - imgui.text(" [tools.text_editors.notepadpp]") - imgui.text(' path = "C:\\\\path\\\\to\\\\notepad++.exe"') - imgui.text(' diff_args = ["-multiInst", "-nosession"]') - imgui.text("") - imgui.text("Then set default in [tools.default_editor]") - else: - imgui.text("Default Editor:") - editor_names = sorted(list(editors.keys())) - if default_name and default_name in editor_names: current_idx = editor_names.index(default_name) - else: current_idx = 0 - changed, new_idx = imgui.combo("##editor_combo", current_idx, editor_names) - if changed: self._set_external_editor_default(editor_names[new_idx]) - imgui.text("") - imgui.text("Configured Editors:") - imgui.separator() - for name in editor_names: - editor = editors.get(name) - if not editor: continue - is_default = name == default_name - marker = " (default)" if is_default else "" - if is_default: imgui.text_colored(C_IN, f" {name}{marker}") - else: imgui.text(f" {name}{marker}") - imgui.text(f" {editor.path}") - if editor.diff_args: imgui.textDisabled(f" diff: {editor.diff_args}") - imgui.text("") - imgui.text("Config: config.toml [tools.text_editors]") - imgui.text("Override: manual_slop.toml default_editor") - except Exception as e: - imgui.text_colored(C_TC, f"Error: {str(e)}") + render_external_editor_panel(self) 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() + render_approve_script_modal(self) #endregion: Misc Tools #region: Sanity Tests def _render_markdown_test(self) -> None: - imgui.text("Markdown Test Panel") - imgui.separator() - md = """ -# Header 1 -## Header 2 -### Header 3 -This is **bold** text and *italic* text. -And ***bold italic*** text. - -* List item 1 -* List item 2 - * Sub-item - -[Link to Google](https://google.com) - -```python -def hello(): - print("Markdown works!") -``` -""" - markdown_helper.render(md) + render_markdown_test(self) #endregion: Sanity Tests @@ -4381,713 +1270,52 @@ def hello(): 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() + render_ticket_queue(self) 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.") + render_task_dag_panel(self) 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}") + render_beads_tab(self) def _render_mma_dashboard(self) -> None: - """ - Main MMA dashboard interface. - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle] - """ - if 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") + render_mma_dashboard(self) def _render_mma_focus_selector(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - 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 + render_mma_focus_selector(self) 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() + render_mma_modals(self) def _render_mma_track_summary(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - 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)") + render_mma_track_summary(self) def _render_mma_epic_planner(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - 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() + render_mma_epic_planner(self) def _render_mma_conductor_setup(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - 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) + render_mma_conductor_setup(self) def _render_mma_track_browser(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Track Browser") - if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() - for track in 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 = "" + render_mma_track_browser(self) def _render_mma_global_controls(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - 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 - imgui.separator() - imgui.text("Hot Reload:") - imgui.same_line() - if imgui.button("Reload GUI"): - success = self._trigger_hot_reload() - if success: - imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Reloaded!") - else: - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error: {self._hot_reload_error or 'Unknown'}") - imgui.same_line(); imgui.text_disabled("(Ctrl+Alt+R)") + render_mma_global_controls(self) def _render_mma_usage_section(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Tier Usage (Tokens & Cost)") - if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): - imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() - total_cost = 0.0 - for tier, stats in 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() + render_mma_usage_section(self) 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() + render_mma_ticket_editor(self) def _render_mma_agent_streams(self) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Agent Streams") - if imgui.begin_tab_bar("mma_streams_tabs"): - for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: - with imscope.tab_item(tier) as (exp, _): - if exp: - sep_val = getattr(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() + render_mma_agent_streams(self) 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) - imgui.end_child() - except (TypeError, AttributeError): - imgui.end_child() - pass - 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]) - imgui.end_child() - except (TypeError, AttributeError): - imgui.end_child() - pass - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") + render_tier_stream_panel(self, tier_key, stream_key) 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() + render_track_proposal_modal(self) def request_patch_from_tier4(self, error: str, file_context: str) -> None: try: @@ -5186,3 +1414,4053 @@ def main() -> None: if __name__ == "__main__": main() + +def render_text_viewer(app: App, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: + if imgui.button("[+]##" + str(id(content))) or force_open: + app.text_viewer_type = text_type + app.show_text_viewer = True + app.text_viewer_title = label + app.text_viewer_content = content + +def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None: + imgui.text_colored(C_LBL, f"{label}:") + imgui.same_line() + if imgui.button("[+]##" + label + id_suffix): + app.show_text_viewer = True + app.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text' + app.text_viewer_title = label + app.text_viewer_content = content + + if not content: + imgui.text_disabled("(empty)") + return + + is_md = label in ("message", "text", "content") + ctx_id = f"heavy_{label}_{id_suffix}" + + with theme.ai_text_style(): + if len(content) > COMMS_CLAMP_CHARS: + if is_md: + imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar) + markdown_helper.render(content, context_id=ctx_id) + imgui.end_child() + else: + imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only) + else: + if is_md: + markdown_helper.render(content, context_id=ctx_id) + else: + if app.ui_word_wrap: + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(content) + imgui.pop_text_wrap_pos() + else: + imgui.text(content) + +def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: + if not segments: + return + with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ + theme.ai_text_style(): + imgui.indent() + + show_content = True + if not is_standalone: + header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" + show_content = imgui.collapsing_header(header_label) + + if show_content: + h = 150 if is_standalone else 100 + with imscope.child(f"thinking_content_{entry_index}", 0, h, True): + for idx, seg in enumerate(segments): + content = seg.get("content", "") + marker = seg.get("marker", "thinking") + with imscope.id(f"think_{entry_index}_{idx}"): + imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") + if app.ui_word_wrap: + with imscope.text_wrap(imgui.get_content_region_avail().x): + imgui.text(content) + else: + imgui.text(content) + imgui.separator() + + imgui.unindent() + +def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None: + with imscope.id(label + str(hash(value))): + with imscope.style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.frame_bg_hovered, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.frame_bg_active, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.border, vec4(0, 0, 0, 0)): + with imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0), \ + imscope.style_var(imgui.StyleVar_.frame_padding, imgui.ImVec2(0, 0)): + if color: + with imscope.style_color(imgui.Col_.text, color): + if multiline: + imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) + else: + if width > 0: imgui.set_next_item_width(width) + imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + else: + if multiline: + imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) + else: + if width > 0: imgui.set_next_item_width(width) + imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + +def render_save_preset_modal(app: App) -> None: + if not app._show_save_preset_modal: return + imgui.open_popup("Save Layout Preset") + with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + imgui.text("Preset Name:") + _, app._new_preset_name = imgui.input_text("##preset_name", app._new_preset_name) + if imgui.button("Save", imgui.ImVec2(120, 0)): + if app._new_preset_name.strip(): + ini_data = imgui.save_ini_settings_to_memory() + app.layout_presets[app._new_preset_name.strip()] = { + "ini": ini_data, + "multi_viewport": app.ui_multi_viewport + } + app.config["layout_presets"] = app.layout_presets + models.save_config(app.config) + app._show_save_preset_modal = False + app._new_preset_name = "" + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + app._show_save_preset_modal = False + imgui.close_current_popup() + +def render_main_interface(app: App) -> None: + render_error_tint(app) + app.perf_monitor.start_frame() + app._autofocus_response_tab = app.controller._autofocus_response_tab + + #region: Process GUI task queue + app._process_pending_gui_tasks() + app._process_pending_history_adds() + if app.controller._process_pending_tool_calls(): app._tool_log_dirty = True + #endregion: Process GUI task queue + + render_track_proposal_modal(app) + render_patch_modal(app) + render_base_prompt_diff_modal(app) + render_save_preset_modal(app) + render_save_workspace_profile_modal(app) + render_add_context_files_modal(app) + render_preset_manager_window(app) + render_tool_preset_manager_window(app) + render_persona_editor_window(app) + + # Auto-save (every 60s) + now = time.time() + if now - app._last_autosave >= app._autosave_interval: + app._last_autosave = now + try: + app._flush_to_project() + app._flush_to_config() + models.save_config(app.config) + except Exception: + pass # silent — don't disrupt the GUI loop + + # Sync pending comms + with app._pending_comms_lock: + if app._pending_comms: + if app.ui_auto_scroll_comms: app._scroll_comms_to_bottom = True + app._comms_log_dirty = True + for c in app._pending_comms: app._comms_log.append(c) + app._pending_comms.clear() + + if app.ui_focus_agent != app._last_ui_focus_agent: + app._comms_log_dirty = True + app._tool_log_dirty = True + app._last_ui_focus_agent = app.ui_focus_agent + + if app._comms_log_dirty: + if app.is_viewing_prior_session: app._comms_log_cache = app.prior_session_entries + else: + log_raw = list(app._comms_log) + if app.ui_focus_agent: app._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] + else: app._comms_log_cache = log_raw + app._comms_log_dirty = False + + if app._tool_log_dirty: + if app.is_viewing_prior_session: app._tool_log_cache = app.prior_tool_calls + else: + log_raw = list(app._tool_log) + if app.ui_focus_agent: app._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] + else: app._tool_log_cache = log_raw + app._tool_log_dirty = False + + app._render_window_if_open("Project Settings", app._render_project_settings_hub) + app._render_window_if_open("Files & Media", app._render_files_and_media) + app._render_window_if_open("AI Settings", app._render_ai_settings_hub) + app._render_window_if_open("Usage Analytics", app._render_usage_analytics_panel, app.ui_separate_usage_analytics) + app._render_window_if_open("MMA Dashboard", app._render_mma_dashboard) + app._render_window_if_open("Task DAG", app._render_task_dag_panel, app.ui_separate_task_dag) + + app._render_window_if_open("Tier 1: Strategy", lambda: render_tier_stream_panel(app, "Tier 1", "Tier 1"), app.ui_separate_tier1) + app._render_window_if_open("Tier 2: Tech Lead", lambda: render_tier_stream_panel(app, "Tier 2", "Tier 2 (Tech Lead)"), app.ui_separate_tier2) + app._render_window_if_open("Tier 3: Workers", lambda: render_tier_stream_panel(app, "Tier 3", None), app.ui_separate_tier3) + app._render_window_if_open("Tier 4: QA", lambda: render_tier_stream_panel(app, "Tier 4", "Tier 4 (QA)"), app.ui_separate_tier4) + + if app.show_windows.get("Theme", False): render_theme_panel(app) + + app._render_window_if_open("Discussion Hub", app._render_discussion_hub) + app._render_window_if_open("Operations Hub", app._render_operations_hub) + + app._render_window_if_open("Message", app._render_message_panel, app.ui_separate_message_panel) + app._render_window_if_open("Response", app._render_response_panel, app.ui_separate_response_panel) + app._render_window_if_open("Tool Calls", app._render_tool_calls_panel, app.ui_separate_tool_calls_panel) + app._render_window_if_open("External Tools", app._render_external_tools_panel, app.ui_separate_external_tools) + app._render_window_if_open("Log Management", app._render_log_management) + app._render_window_if_open("Diagnostics", app._render_diagnostics_panel) + + app.perf_monitor.end_frame() + + # Modals / Popups + render_approve_script_modal(app) + render_mma_modals(app) + +def render_custom_title_bar(app: App) -> None: + # Obsolete, removed since it renders behind the full screen dock space. + # Controls are now embedded in _show_menus. + pass + +def render_history_window(app: App) -> None: + if not app.show_windows.get('Undo/Redo History', False): + return + def iterate_history(history: typing.List[typing.Dict[str, typing.Any]]) -> None: + for i, entry in enumerate(reversed(history)): + actual_idx = len(history) - 1 - i + desc = entry.get("description", "UI Change") + ts = entry.get("timestamp", 0.0) + ts_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") + label = f"[{ts_str}] {desc}##{actual_idx}" + _, selected = imgui.selectable(label, False) + if selected: app._handle_jump_to_history(actual_idx) + with imscope.window("Undo/Redo History", app.show_windows['Undo/Redo History']) as (exp, opened): + app.show_windows['Undo/Redo History'] = bool(opened) + if exp: + if imgui.button("Undo") and app.history.can_undo: app._handle_undo() + imgui.same_line() + if imgui.button("Redo") and app.history.can_redo: app._handle_redo() + imgui.separator() + with imscope.child("history_list", 0, 0, True): + history = app.history.get_history() + if not history: imgui.text("No history available.") + else: iterate_history() + +def render_theme_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_theme_panel") + exp, opened = imgui.begin("Theme", app.show_windows["Theme"]) + app.show_windows["Theme"] = bool(opened) + if exp: + imgui.text("Palette") + cp = theme.get_current_palette() + if imgui.begin_combo("##pal", cp): + for p in theme.get_palette_names(): + if imgui.selectable(p, p == cp)[0]: + theme.apply(p) + app._flush_to_config() + models.save_config(app.config) + imgui.end_combo() + + imgui.separator() + ch1, app.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", app.ui_separate_message_panel) + ch2, app.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", app.ui_separate_response_panel) + ch3, app.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", app.ui_separate_tool_calls_panel) + if ch1: app.show_windows["Message"] = app.ui_separate_message_panel + if ch2: app.show_windows["Response"] = app.ui_separate_response_panel + if ch3: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel + imgui.separator() + imgui.text("Font") + imgui.push_item_width(-150) + ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) + imgui.pop_item_width() + if ch: theme._current_font_path = path + imgui.same_line() + if imgui.button("Browse##font"): + r = hide_tk_root() + p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) + r.destroy() + if p: theme._current_font_path = p + imgui.text("Size (px)") + imgui.same_line() + imgui.push_item_width(100) + ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") + if ch: theme._current_font_size = size + imgui.pop_item_width() + imgui.same_line() + if imgui.button("Apply Font (Requires Restart)"): + app._flush_to_config() + models.save_config(app.config) + app.ai_status = "Font settings saved. Restart required." + imgui.separator() + imgui.text("UI Scale (DPI)") + ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") + if ch: + theme.set_scale(scale) + app._flush_to_config() + models.save_config(app.config) + + imgui.text("Panel Transparency") + ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f") + if ch_t: + theme.set_transparency(trans) + app._flush_to_config() + models.save_config(app.config) + + imgui.text("Panel Item Transparency") + ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f") + if ch_ct: + theme.set_child_transparency(ctrans) + bg = bg_shader.get_bg() + ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled) + if ch_bg: + gui_cfg = app.config.setdefault("gui", {}) + gui_cfg["bg_shader_enabled"] = bg.enabled + app._flush_to_config() + models.save_config(app.config) + + ch_crt, app.ui_crt_filter = imgui.checkbox("CRT Filter", app.ui_crt_filter) + if ch_crt: + gui_cfg = app.config.setdefault("gui", {}) + gui_cfg["crt_filter_enabled"] = app.ui_crt_filter + app._flush_to_config() + models.save_config(app.config) + + imgui.end() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel") + +def render_shader_live_editor(app: App) -> None: + """ + [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] + """ + if app.show_windows.get('Shader Editor', False): + with imscope.window('Shader Editor', app.show_windows['Shader Editor']) as (exp, opened): + app.show_windows['Shader Editor'] = bool(opened) + if exp: + changed_crt, app.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', app.shader_uniforms['crt'], 0.0, 2.0) + changed_scan, app.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', app.shader_uniforms['scanline'], 0.0, 1.0) + changed_bloom, app.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', app.shader_uniforms['bloom'], 0.0, 1.0) + +def render_usage_analytics_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_usage_analytics_panel") + render_token_budget_panel(app) + imgui.separator() + render_cache_panel(app) + imgui.separator() + render_tool_analytics_panel(app) + imgui.separator() + render_session_insights_panel(app) + imgui.separator() + + # RAG status indicator + if app.controller.rag_config and app.controller.rag_config.enabled: + # imgui.same_line() + status = app.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(): app.controller.event_queue.put('click', 'btn_rebuild_rag_index') + + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_usage_analytics_panel") + +def render_cache_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_cache_panel") + if app.current_provider != "gemini": + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + return + imgui.text_colored(C_LBL, 'Cache Analytics') + stats = getattr(app.controller, '_cached_cache_stats', {}) + if not stats.get("cache_exists"): + imgui.text_disabled("No active cache") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + return + age_sec = stats.get("cache_age_seconds", 0) + ttl_remaining = stats.get("ttl_remaining", 0) + ttl_total = stats.get("ttl_seconds", 3600) + age_str = f"{age_sec/60:.0f}m {age_sec%60:.0f}s" + remaining_str = f"{ttl_remaining/60:.0f}m {ttl_remaining%60:.0f}s" + ttl_pct = (ttl_remaining / ttl_total * 100) if ttl_total > 0 else 0 + imgui.text(f"Age: {age_str}") + imgui.text(f"TTL: {remaining_str} ({ttl_pct:.0f}%)") + color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) + if ttl_pct < 20: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) + elif ttl_pct < 50: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) + imgui.push_style_color(imgui.Col_.plot_histogram, color) + imgui.progress_bar(ttl_pct / 100.0, imgui.ImVec2(-1, 0), f"{ttl_pct:.0f}%") + imgui.pop_style_color() + if imgui.button("Clear Cache"): + app.controller.clear_cache() + app._cache_cleared_timestamp = time.time() + if hasattr(app, '_cache_cleared_timestamp') and time.time() - app._cache_cleared_timestamp < 5: + imgui.text_colored(imgui.ImVec4(0.2, 1.0, 0.2, 1.0), "Cache cleared - will rebuild on next request") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + +def render_diagnostics_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_diagnostics_panel") + with imscope.window("Diagnostics", app.show_windows.get("Diagnostics", False)) as (exp, opened): + app.show_windows["Diagnostics"] = bool(opened) + if exp: + metrics = app.perf_monitor.get_metrics() + imgui.text("Performance Telemetry") + imgui.same_line() + _, app.perf_profiling_enabled = imgui.checkbox("Enable Profiling", app.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() + app.perf_show_graphs.setdefault(key, False) + _, app.perf_show_graphs[key] = imgui.checkbox(f"##g_{key}", app.perf_show_graphs[key]) + imgui.end_table() + + if app.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() + app.perf_show_graphs.setdefault(comp_name, False) + _, app.perf_show_graphs[comp_name] = imgui.checkbox(f"##g_{comp_name}", app.perf_show_graphs[comp_name]) + imgui.end_table() + + imgui.separator() + imgui.text("Performance Graphs") + for key, show in app.perf_show_graphs.items(): + if show: + imgui.text(f"History: {key}") + hist_data = app.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(app.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 app.perf_profiling_enabled: app.perf_monitor.end_component("_render_diagnostics_panel") + +def render_tool_analytics_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_analytics_panel") + imgui.text_colored(C_LBL, 'Tool Usage') + imgui.separator() + now = time.time() + if not hasattr(app, '_tool_stats_cache_time') or now - app._tool_stats_cache_time > 1.0: + app._cached_tool_stats = getattr(app.controller, '_tool_stats', {}) + tool_stats = getattr(app.controller, '_cached_tool_stats', {}) + if not tool_stats: + imgui.text_disabled("No tool usage data") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") + return + if imgui.begin_table("tool_stats", 4, imgui.TableFlags_.borders | imgui.TableFlags_.sortable): + imgui.table_setup_column("Tool") + imgui.table_setup_column("Count") + imgui.table_setup_column("Avg (ms)") + imgui.table_setup_column("Fail %") + imgui.table_headers_row() + sorted_tools = sorted(tool_stats.items(), key=lambda x: -x[1].get("count", 0)) + for tool_name, stats in sorted_tools: + count = stats.get("count", 0) + total_time = stats.get("total_time_ms", 0) + failures = stats.get("failures", 0) + avg_time = total_time / count if count > 0 else 0 + fail_pct = (failures / count * 100) if count > 0 else 0 + imgui.table_next_row() + imgui.table_set_column_index(0) + imgui.text(tool_name) + imgui.table_set_column_index(1) + imgui.text(str(count)) + imgui.table_set_column_index(2) + imgui.text(f"{avg_time:.0f}") + imgui.table_set_column_index(3) + if fail_pct > 0: imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{fail_pct:.0f}%") + else: imgui.text("0%") + imgui.end_table() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") + +def render_token_budget_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_token_budget_panel") + imgui.text_colored(C_LBL, 'Prompt Utilization') + usage = app.session_usage + total = usage["input_tokens"] + usage["output_tokens"] + if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"] + render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES) + if usage.get("last_latency", 0.0) > 0: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s") + if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") + if app._gemini_cache_text: imgui.text_colored(C_SUB, app._gemini_cache_text) + imgui.separator() + + if app._token_stats_dirty: + app._token_stats_dirty = False + # Offload to background thread via event queue + app.controller.event_queue.put("refresh_api_metrics", {"md_content": app._last_stable_md or ""}) + stats = app._token_stats + if not stats: + imgui.text_disabled("Token stats unavailable") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") + return + pct = stats.get("utilization_pct", 0.0) + current = stats.get("estimated_prompt_tokens", stats.get("total_tokens", 0)) + limit = stats.get("max_prompt_tokens", 0) + headroom = stats.get("headroom_tokens", max(0, limit - current)) + if pct < 50.0: color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) + elif pct < 80.0: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) + else: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) + imgui.push_style_color(imgui.Col_.plot_histogram, color) + imgui.progress_bar(pct / 100.0, imgui.ImVec2(-1, 0), f"{pct:.1f}%") + imgui.pop_style_color() + imgui.text_disabled(f"{current:,} / {limit:,} tokens ({headroom:,} remaining)") + sys_tok = stats.get("system_tokens", 0) + tool_tok = stats.get("tools_tokens", 0) + hist_tok = stats.get("history_tokens", 0) + total_tok = sys_tok + tool_tok + hist_tok or 1 + if imgui.begin_table("token_breakdown", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): + imgui.table_setup_column("Component") + imgui.table_setup_column("Tokens") + imgui.table_setup_column("Pct") + imgui.table_headers_row() + for lbl, tok in [("System", sys_tok), ("Tools", tool_tok), ("History", hist_tok)]: + imgui.table_next_row() + imgui.table_set_column_index(0); imgui.text(lbl) + imgui.table_set_column_index(1); imgui.text(f"{tok:,}") + imgui.table_set_column_index(2); imgui.text(f"{tok / total_tok * 100:.0f}%") + imgui.end_table() + imgui.separator() + imgui.text("MMA Tier Costs") + if hasattr(app, 'mma_tier_usage') and app.mma_tier_usage: + if imgui.begin_table("tier_cost_breakdown", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): + imgui.table_setup_column("Tier") + imgui.table_setup_column("Model") + imgui.table_setup_column("Tokens") + imgui.table_setup_column("Est. Cost") + imgui.table_headers_row() + for tier, stats in app.mma_tier_usage.items(): + model = stats.get('model', 'unknown') + in_t = stats.get('input', 0) + out_t = stats.get('output', 0) + tokens = in_t + out_t + cost = cost_tracker.estimate_cost(model, in_t, out_t) + imgui.table_next_row() + imgui.table_set_column_index(0); render_selectable_label(app, f"tier_{tier}", tier, width=-1) + imgui.table_set_column_index(1); render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1) + imgui.table_set_column_index(2); render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1) + imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1)) + imgui.end_table() + tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in app.mma_tier_usage.values()) + render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1)) + else: + imgui.text_disabled("No MMA tier usage data") + if stats.get("would_trim"): + imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.0, 1.0), "WARNING: Next call will trim history") + trimmable = stats.get("trimmable_turns", 0) + if trimmable: imgui.text_disabled(f"Trimmable turns: {trimmable}") + msgs = stats.get("messages") + if msgs: + shown = 0 + for msg in msgs: + if shown >= 3: break + if msg.get("trimmable"): + role = msg.get("role", "?") + toks = msg.get("tokens", 0) + imgui.text_disabled(f" [{role}] ~{toks:,} tokens") + shown += 1 + imgui.separator() + cache_stats = getattr(app.controller, '_cached_cache_stats', {}) + if cache_stats.get("cache_exists"): + age = cache_stats.get("cache_age_seconds", 0) + ttl = cache_stats.get("ttl_seconds", 3600) + imgui.text_colored(C_LBL, f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s") + else: + imgui.text_disabled("Cache Usage: INACTIVE") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") + +def render_session_insights_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_session_insights_panel") + imgui.text_colored(C_LBL, 'Session Insights') + imgui.separator() + insights = app.controller.get_session_insights() + imgui.text(f"Total Tokens: {insights.get('total_tokens', 0):,}") + imgui.text(f"API Calls: {insights.get('call_count', 0)}") + imgui.text(f"Burn Rate: {insights.get('burn_rate', 0):.0f} tokens/min") + imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}") + completed = insights.get('completed_tickets', 0) + efficiency = insights.get('efficiency', 0) + imgui.text(f"Completed: {completed}") + imgui.text(f"Tokens/Ticket: {efficiency:.0f}" if efficiency > 0 else "Tokens/Ticket: N/A") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_session_insights_panel") + +def render_log_management(app: App) -> None: + """ + [C: tests/test_log_management_ui.py:test_render_log_management_logic] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_log_management") + with imscope.window("Log Management", app.show_windows["Log Management"]) as (exp, opened): + app.show_windows["Log Management"] = bool(opened) + if exp: + if app._log_registry is None: app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + else: + if imgui.button("Refresh Registry"): app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + imgui.same_line() + if imgui.button("Load Log"): app.cb_load_prior_log() + imgui.same_line() + if imgui.button("Force Prune Logs"): app.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) + + registry = app._log_registry + sessions = registry.data + if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Session ID") + imgui.table_setup_column("Start Time") + imgui.table_setup_column("Star") + imgui.table_setup_column("Reason") + imgui.table_setup_column("Size (KB)") + imgui.table_setup_column("Msgs") + imgui.table_setup_column("Actions") + imgui.table_headers_row() + for session_id, s_data in sessions.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(session_id) + imgui.table_next_column() + imgui.text(s_data.get("start_time", "")) + imgui.table_next_column() + whitelisted = s_data.get("whitelisted", False) + if whitelisted: + imgui.text_colored(vec4(255, 215, 0), "YES") + else: + imgui.text("NO") + metadata = s_data.get("metadata") or {} + imgui.table_next_column() + imgui.text(metadata.get("reason", "")) + imgui.table_next_column() + imgui.text(str(metadata.get("size_kb", ""))) + imgui.table_next_column() + imgui.text(str(metadata.get("message_count", ""))) + imgui.table_next_column() + if imgui.button(f"Load##{session_id}"): + app.cb_load_prior_log(s_data.get("path")) + imgui.same_line() + if whitelisted: + if imgui.button(f"Unstar##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=False, + reason=str(metadata.get("reason") or "") + ) + else: + if imgui.button(f"Star##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=True, + reason="Manually whitelisted" + ) + imgui.end_table() + + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management") + +def render_project_settings_hub(app: App) -> None: + with imscope.tab_bar('context_hub_tabs'): + with imscope.tab_item('Projects') as (exp, _): + if exp: render_projects_panel(app) + with imscope.tab_item('Paths') as (exp, _): + if exp: render_paths_panel(app) + +def render_projects_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_projects_panel") + proj_name = app.project.get("project", {}).get("name", Path(app.active_project_path).stem) + imgui.text_colored(C_IN, f"Active: {proj_name}") + imgui.separator() + imgui.text("Execution Mode") + modes = ["native", "beads"] + current_idx = modes.index(app.ui_project_execution_mode) if app.ui_project_execution_mode in modes else 0 + ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) + if ch: app.ui_project_execution_mode = modes[new_idx] + imgui.separator() + imgui.text("Git Directory") + ch, app.ui_project_git_dir = imgui.input_text("##git_dir", app.ui_project_git_dir) + imgui.same_line() + if imgui.button("Browse##git"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Git Directory") + r.destroy() + if d: app.ui_project_git_dir = d + imgui.separator() + imgui.text("Output Dir") + ch, app.ui_output_dir = imgui.input_text("##out_dir", app.ui_output_dir) + imgui.same_line() + if imgui.button("Browse##out"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Output Dir") + r.destroy() + if d: app.ui_output_dir = d + imgui.separator() + imgui.text("Conductor Directory") + ch, app.ui_project_conductor_dir = imgui.input_text("##cond_dir", app.ui_project_conductor_dir) + imgui.same_line() + if imgui.button("Browse##cond"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Conductor Directory") + r.destroy() + if d: app.ui_project_conductor_dir = d + imgui.separator() + imgui.text("Project Files") + imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) + for i, pp in enumerate(app.project_paths): + is_active = (pp == app.active_project_path) + if imgui.button(f"x##p{i}"): + removed = app.project_paths.pop(i) + if removed == app.active_project_path and app.project_paths: app._switch_project(app.project_paths[0]) + break + imgui.same_line() + marker = " *" if is_active else "" + if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) + if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): app._switch_project(pp) + if is_active: imgui.pop_style_color() + imgui.same_line() + imgui.text_colored(C_LBL, pp) + imgui.end_child() + if imgui.button("Add Project"): + r = hide_tk_root() + p = filedialog.askopenfilename( + title="Select Project .toml", + filetypes=[("TOML", "*.toml"), ("All", "*.*")], + ) + r.destroy() + if p and p not in app.project_paths: + app.project_paths.append(p) + imgui.same_line() + if imgui.button("New Project"): + r = hide_tk_root() + p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) + r.destroy() + if p: + name = Path(p).stem + proj = project_manager.default_project(name) + project_manager.save_project(proj, p) + if p not in app.project_paths: app.project_paths.append(p) + app._switch_project(p) + imgui.same_line() + if imgui.button("Save All"): + app._flush_to_project() + app._flush_to_config() + models.save_config(app.config) + app.ai_status = "config saved" + ch, app.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", app.ui_word_wrap) + ch, app.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", app.ui_auto_scroll_comms) + ch, app.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", app.ui_auto_scroll_tool_calls) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_projects_panel") + +def render_paths_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_paths_panel") + path_info = paths.get_full_path_info() + + imgui.text_colored(C_IN, "System Path Configuration") + imgui.separator() + +def render_path_field(label: str, attr: str, key: str, tooltip: str): + info = path_info.get(key, {'source': 'unknown'}) + imgui.text(label) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + imgui.same_line() + imgui.text_disabled(f"(Source: {info['source']})") + + val = getattr(app, attr) + changed, new_val = imgui.input_text(f"##{key}", val) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + if changed: setattr(app, attr, new_val) + imgui.same_line() + if imgui.button(f"Browse##{key}"): + r = hide_tk_root() + d = filedialog.askdirectory(title=f"Select {label}") + r.destroy() + if d: setattr(app, attr, d) + +def render_external_tools_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_external_tools_panel") + if imgui.button("Refresh External MCPs"): app.event_queue.put("refresh_external_mcps", None) + + imgui.separator() + + # Server status indicators + manager = mcp_client.get_external_mcp_manager() + statuses = manager.get_servers_status() + if statuses: + imgui.text("Servers:") + for sname, status in statuses.items(): + imgui.same_line() + # Green for running, Yellow for starting, Red for error, Gray for idle + col = (0.5, 0.5, 0.5, 1.0) + if status == 'running': col = (0.0, 1.0, 0.0, 1.0) + elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0) + elif status == 'error': col = (1.0, 0.0, 0.0, 1.0) + imgui.color_button(f"##status_{sname}", col) + imgui.same_line() + imgui.text(sname) + imgui.separator() + + tools = manager.get_all_tools() + if not tools: + imgui.text_disabled("No external tools found.") + else: + if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Name") + imgui.table_setup_column("Server") + imgui.table_setup_column("Description") + imgui.table_headers_row() + + for tname, tinfo in tools.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(tname) + imgui.table_next_column() + imgui.text(tinfo.get('server', 'unknown')) + imgui.table_next_column() + imgui.text(tinfo.get('description', '')) + imgui.end_table() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_external_tools_panel") + +def render_ai_settings_hub(app: App) -> None: + render_persona_selector_panel(app) + if imgui.collapsing_header("Provider & Model"): render_provider_panel(app) + if imgui.collapsing_header("System Prompts"): render_system_prompts_panel(app) + if imgui.collapsing_header("RAG Settings"): render_rag_panel(app) + render_agent_tools_panel(app) + +def render_rag_panel(app: App) -> None: + conf = app.controller.rag_config + if not conf: return + ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled) + + imgui.text("Vector Store Provider") + providers = ['chroma', 'qdrant', 'mock'] + try: + idx = providers.index(conf.vector_store.provider) + except (ValueError, AttributeError): + idx = 0 + ch2, next_idx = imgui.combo("##rag_provider", idx, providers) + if ch2: + conf.vector_store.provider = providers[next_idx] + + imgui.text("Embedding Provider") + emb_providers = ['gemini', 'local'] + try: + idx_e = emb_providers.index(conf.embedding_provider) + except (ValueError, AttributeError): + idx_e = 0 + ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers) + if ch3: + conf.embedding_provider = emb_providers[next_idx_e] + + imgui.text("Chunk Size") + imgui.set_next_item_width(150) + ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size) + imgui.text("Chunk Overlap") + imgui.set_next_item_width(150) + ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap) + + imgui.separator() + imgui.text(f"Status: {app.controller.rag_status}") + + if imgui.button("Rebuild Index"): app.controller.event_queue.put('click', 'btn_rebuild_rag_index') + +def render_system_prompts_panel(app: App) -> None: + imgui.text("Global System Prompt (all projects)") + preset_names = sorted(app.controller.presets.keys()) + current_global = app.controller.ui_global_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##global_preset", current_global): + for name in preset_names: + is_sel = (name == current_global) + if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "global") + if is_sel: imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##global"): app.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, app.ui_global_system_prompt = imgui.input_text_multiline("##gsp", app.ui_global_system_prompt, imgui.ImVec2(-1, 100)) + imgui.separator() + _, app.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", app.ui_use_default_base_prompt) + imgui.same_line() + if imgui.button("Reset to Default##btn_reset_base_prompt"): app.controller._cb_reset_base_prompt() + imgui.same_line() + if imgui.button("Show Diff##btn_show_base_prompt_diff"): app.controller._cb_show_base_prompt_diff() + imgui.set_item_tooltip("Compare current base prompt with the default.") + + imgui.same_line() + imgui.text_disabled("(?)") + imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.") + + header_flags = imgui.TreeNodeFlags_.default_open if not app.ui_use_default_base_prompt else 0 + if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags): + if app.ui_use_default_base_prompt: + imgui.begin_disabled() + imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only) + imgui.end_disabled() + imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}") + else: + ch, app.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", app.ui_base_system_prompt, imgui.ImVec2(-1, 150)) + imgui.text_disabled(f"Characters: {len(app.ui_base_system_prompt)}") + imgui.separator() + imgui.text("Project System Prompt") + current_project = app.controller.ui_project_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##project_preset", current_project): + for name in preset_names: + is_sel = (name == current_project) + if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "project") + if is_sel: imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##project"): app.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, app.ui_project_system_prompt = imgui.input_text_multiline("##psp", app.ui_project_system_prompt, imgui.ImVec2(-1, 100)) + +def render_agent_tools_panel(app: App) -> None: + if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): + imgui.text("Tool Preset") + presets = app.controller.tool_presets + preset_names = [""] + sorted(list(presets.keys())) + + active = getattr(app, "ui_active_tool_preset", "") + if active is None: active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + + ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names) + if ch: + app.ui_active_tool_preset = preset_names[new_idx] + + imgui.same_line() + if imgui.button("Manage Presets##tools"): app.show_tool_preset_manager_window = True + if imgui.is_item_hovered(): imgui.set_tooltip("Configure tool availability and default modes.") + + imgui.dummy(imgui.ImVec2(0, 4)) + imgui.text("Bias Profile") + if imgui.begin_combo("##bias", getattr(app, 'ui_active_bias_profile', "") or "None"): + if imgui.selectable("None", not getattr(app, 'ui_active_bias_profile', ""))[0]: + app.ui_active_bias_profile = "" + ai_client.set_bias_profile(None) + for bname in sorted(app.controller.bias_profiles.keys()): + if not bname: continue + if imgui.selectable(bname, bname == getattr(app, 'ui_active_bias_profile', ""))[0]: + app.ui_active_bias_profile = bname + ai_client.set_bias_profile(bname) + imgui.end_combo() + + imgui.dummy(imgui.ImVec2(0, 8)) + cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) + try: + f_idx = cat_options.index(app.ui_tool_filter_category) + except ValueError: + f_idx = 0 + imgui.set_next_item_width(200) + ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options) + if ch_cat: app.ui_tool_filter_category = cat_options[next_f_idx] + + imgui.dummy(imgui.ImVec2(0, 8)) + active_name = app.ui_active_tool_preset + if active_name and active_name in presets: + preset = presets[active_name] + for cat_name, tools in preset.categories.items(): + if app.ui_tool_filter_category != "All" and app.ui_tool_filter_category != cat_name: continue + if imgui.tree_node(cat_name): + for tool in tools: + if tool.weight >= 5: imgui.text_colored(vec4(255, 100, 100), "[HIGH]"); imgui.same_line() + elif tool.weight == 4: imgui.text_colored(vec4(255, 255, 100), "[PREF]"); imgui.same_line() + elif tool.weight == 2: imgui.text_colored(vec4(255, 150, 50), "[REJECT]"); imgui.same_line() + elif tool.weight <= 1: imgui.text_colored(vec4(180, 180, 180), "[LOW]"); imgui.same_line() + + imgui.text(tool.name); imgui.same_line(180) + + mode = tool.approval + if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): tool.approval = "auto" + imgui.same_line() + if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): tool.approval = "ask" + imgui.tree_pop() + +def render_preset_manager_content(app: App, is_embedded: bool = False) -> None: + avail = imgui.get_content_region_avail() + if not hasattr(app, "_prompt_md_preview"): app._prompt_md_preview = False + + if imgui.begin_table("prompt_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row() + + # Left Sidebar + imgui.table_next_column() + imgui.begin_child("prompt_list_pane", imgui.ImVec2(0, 0), False) + if True: + if imgui.button("New Preset", imgui.ImVec2(-1, 0)): + app._editing_preset_name = "" + app._editing_preset_system_prompt = "" + app._editing_preset_scope = "project" + app._selected_preset_idx = -1 + imgui.separator() + preset_names = sorted(app.controller.presets.keys()) + for i, name in enumerate(preset_names): + if name and imgui.selectable(f"{name}##p_{i}", app._selected_preset_idx == i)[0]: + app._selected_preset_idx = i + app._editing_preset_name = name + p = app.controller.presets[name] + app._editing_preset_system_prompt = p.system_prompt + app._editing_preset_scope = app.controller.preset_manager.get_preset_scope(name) + imgui.end_child() + + # Right Editor + imgui.table_next_column() + avail_r = imgui.get_content_region_avail() + imgui.begin_child("prompt_edit_pane", imgui.ImVec2(0, avail_r.y - 45), False) + if True: + p_disp = app._editing_preset_name or "(New Preset)" + imgui.text_colored(C_IN, f"Editing Prompt Preset: {p_disp}") + imgui.separator() + + if imgui.begin_table("p_meta", 2): + imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row() + imgui.table_next_column(); imgui.text("Name:") + imgui.table_next_column(); imgui.set_next_item_width(-1) + _, app._editing_preset_name = imgui.input_text("##epn", app._editing_preset_name) + imgui.table_next_row() + imgui.table_next_column(); imgui.text("Scope:") + imgui.table_next_column() + if imgui.radio_button("Global##ps", app._editing_preset_scope == "global"): app._editing_preset_scope = "global" + imgui.same_line() + if imgui.radio_button("Project##ps", app._editing_preset_scope == "project"): app._editing_preset_scope = "project" + imgui.end_table() + + imgui.dummy(imgui.ImVec2(0, 4)) + imgui.separator() + imgui.text("Prompt Content:") + imgui.same_line() + if imgui.button("Pop out MD Preview"): + app.text_viewer_title = f"Preset: {app._editing_preset_name}" + app.text_viewer_content = app._editing_preset_system_prompt + app.text_viewer_type = "markdown" + app.show_text_viewer = True + + rem_y = imgui.get_content_region_avail().y + _, app._editing_preset_system_prompt = imgui.input_text_multiline("##pcont", app._editing_preset_system_prompt, imgui.ImVec2(-1, rem_y)) + imgui.end_child() + + # Footer Buttons + imgui.separator() + imgui.dummy(imgui.ImVec2(0, 4)) + if imgui.button("Save##p", imgui.ImVec2(100, 0)): + if app._editing_preset_name.strip(): + app.controller._cb_save_preset( + app._editing_preset_name.strip(), + app._editing_preset_system_prompt, + app._editing_preset_scope + ) + app.ai_status = f"Saved: {app._editing_preset_name}" + imgui.same_line() + if imgui.button("Delete##p", imgui.ImVec2(100, 0)): + if app._editing_preset_name: + app.controller._cb_delete_preset(app._editing_preset_name, app._editing_preset_scope) + app._editing_preset_name = "" + app._selected_preset_idx = -1 + if not is_embedded: + imgui.same_line() + if imgui.button("Close##p", imgui.ImVec2(100, 0)): app.show_preset_manager_window = False + imgui.end_table() + +def render_preset_manager_window(app: App, is_embedded: bool = False) -> None: + if not app.show_preset_manager_window and not is_embedded: return + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) + with imscope.window("Prompt Presets Manager", app.show_preset_manager_window) as (opened, visible): + app.show_preset_manager_window = visible + if opened: render_preset_manager_content(app, is_embedded=is_embedded) + else: + render_preset_manager_content(app, is_embedded=is_embedded) + +def render_tool_preset_manager_content(app: App, is_embedded: bool = False) -> None: + avail = imgui.get_content_region_avail() + if not hasattr(app, "_tool_split_v"): app._tool_split_v = 0.4 + if not hasattr(app, "_bias_split_v"): app._bias_split_v = 0.6 + if not hasattr(app, "_tool_list_open"): app._tool_list_open = True + if not hasattr(app, "_bias_list_open"): app._bias_list_open = True + if not hasattr(app, "_bias_weights_open"): app._bias_weights_open = True + if not hasattr(app, "_bias_cats_open"): app._bias_cats_open = True + + if imgui.begin_table("tp_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row() + + # Left Sidebar + imgui.table_next_column() + imgui.begin_child("tp_list_pane", imgui.ImVec2(0, 0), False) + if True: + if imgui.button("New Preset", imgui.ImVec2(-1, 0)): + app._editing_tool_preset_name = ""; app._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES} + app._editing_tool_preset_scope = "project"; app._selected_tool_preset_idx = -1 + imgui.separator() + preset_names = sorted(app.controller.tool_presets.keys()) + for i, name in enumerate(preset_names): + if name and imgui.selectable(f"{name}##tp_{i}", app._selected_tool_preset_idx == i)[0]: + app._selected_tool_preset_idx = i; app._editing_tool_preset_name = name + preset = app.controller.tool_presets[name] + app._editing_tool_preset_categories = {cat: copy.deepcopy(tools) for cat, tools in preset.categories.items()} + imgui.end_child() + + # Right Editor + imgui.table_next_column() + avail_r = imgui.get_content_region_avail() + imgui.begin_child("tp_editor_content", imgui.ImVec2(0, avail_r.y - 45), False) + if True: + p_name = app._editing_tool_preset_name or "(New Tool Preset)" + imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}"); imgui.separator() + + if imgui.begin_table("tp_meta", 2): + imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, app._editing_tool_preset_name = imgui.input_text("##etpn", app._editing_tool_preset_name) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column() + if imgui.radio_button("Global", app._editing_tool_preset_scope == "global"): app._editing_tool_preset_scope = "global" + imgui.same_line(); + if imgui.radio_button("Project", app._editing_tool_preset_scope == "project"): app._editing_tool_preset_scope = "project" + imgui.end_table() + + rem_y = imgui.get_content_region_avail().y - 80 + if app._tool_list_open and app._bias_list_open: h1, h2 = rem_y * app._tool_split_v, rem_y - (rem_y * app._tool_split_v) - 10 + elif app._tool_list_open: h1, h2 = rem_y, 0 + elif app._bias_list_open: h1, h2 = 0, rem_y + else: h1, h2 = 0, 0 + + imgui.dummy(imgui.ImVec2(0, 4)) + opened_t = imgui.collapsing_header("Categories & Tools", imgui.TreeNodeFlags_.default_open) + if opened_t != app._tool_list_open: app._tool_list_open = opened_t + if app._tool_list_open: + imgui.text("Filter:"); imgui.same_line() + cat_opts = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) + f_idx = cat_opts.index(app.ui_tool_filter_category) if app.ui_tool_filter_category in cat_opts else 0 + imgui.set_next_item_width(200); ch_cat, next_f_idx = imgui.combo("##tp_filter", f_idx, cat_opts) + if ch_cat: app.ui_tool_filter_category = cat_opts[next_f_idx] + imgui.begin_child("tp_scroll", imgui.ImVec2(0, h1), True) + if True: + for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): + if app.ui_tool_filter_category != "All" and app.ui_tool_filter_category != cat_name: continue + if imgui.tree_node(cat_name): + if cat_name not in app._editing_tool_preset_categories: app._editing_tool_preset_categories[cat_name] = [] + curr_cat_tools = app._editing_tool_preset_categories[cat_name] + if imgui.begin_table(f"tt_{cat_name}", 2, imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("Tool", imgui.TableColumnFlags_.width_fixed, 250); imgui.table_setup_column("Ctrls", imgui.TableColumnFlags_.width_stretch) + for tool_name in default_tools: + tool = next((t for t in curr_cat_tools if t.name == tool_name), None) + mode = "disabled" if tool is None else tool.approval + imgui.table_next_row(); imgui.table_next_column(); imgui.text(tool_name); imgui.table_next_column() + if imgui.radio_button(f"Off##{cat_name}_{tool_name}", mode == "disabled"): + if tool: curr_cat_tools.remove(tool) + imgui.same_line(); + if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"): + if not tool: tool = models.Tool(name=tool_name, approval="auto"); current_cat_tools.append(tool) + else: tool.approval = "auto" + imgui.same_line(); + if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"): + if not tool: tool = models.Tool(name=tool_name, approval="ask"); current_cat_tools.append(tool) + else: tool.approval = "ask" + imgui.end_table() + imgui.tree_pop() + imgui.end_child() + if app._bias_list_open: + imgui.button("###tool_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): app._tool_split_v = max(0.1, min(0.9, app._tool_split_v + imgui.get_io().mouse_delta.y / rem_y)) + + imgui.dummy(imgui.ImVec2(0, 4)) + opened_b = imgui.collapsing_header("Bias Profiles", imgui.TreeNodeFlags_.default_open) + if opened_b != app._bias_list_open: app._bias_list_open = opened_b + if app._bias_list_open: + imgui.begin_child("bias_area", imgui.ImVec2(0, h2), True) + if True: + if imgui.begin_table("bias_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("BList", imgui.TableColumnFlags_.width_fixed, 150); imgui.table_setup_column("BEdit", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row(); imgui.table_next_column() + imgui.begin_child("blist_pane", imgui.ImVec2(0, 0), False) + if True: + if imgui.button("New Profile", imgui.ImVec2(-1, 0)): + app._editing_bias_profile_name = ""; app._editing_bias_profile_tool_weights = {} + app._editing_bias_profile_category_multipliers = {}; app._selected_bias_profile_idx = -1 + imgui.separator(); bnames = sorted(app.bias_profiles.keys()) + for i, bname in enumerate(bnames): + if bname and imgui.selectable(f"{bname}##b_{i}", app._selected_bias_profile_idx == i)[0]: + app._selected_bias_profile_idx = i; app._editing_bias_profile_name = bname; prof = app.bias_profiles[bname] + app._editing_bias_profile_tool_weights = copy.deepcopy(prof.tool_weights); app._editing_bias_profile_category_multipliers = copy.deepcopy(prof.category_multipliers) + imgui.end_child() + + imgui.table_next_column() + imgui.begin_child("bedit_pane", imgui.ImVec2(0, 0), False) + if True: + imgui.text("Name:"); imgui.same_line(); imgui.set_next_item_width(-1); _, app._editing_bias_profile_name = imgui.input_text("##bname", app._editing_bias_profile_name) + rem_bias_y = imgui.get_content_region_avail().y - 45 + if app._bias_weights_open and app._bias_cats_open: bh1, bh2 = rem_bias_y * app._bias_split_v, rem_bias_y - (rem_bias_y * app._bias_split_v) - 10 + elif app._bias_weights_open: bh1, bh2 = rem_bias_y, 0 + elif app._bias_cats_open: bh1, bh2 = 0, rem_bias_y + else: bh1, bh2 = 0, 0 + + opened_bw = imgui.collapsing_header("Tool Weights", imgui.TreeNodeFlags_.default_open) + if opened_bw != app._bias_weights_open: app._bias_weights_open = opened_bw + if app._bias_weights_open: + imgui.begin_child("btool_scroll", imgui.ImVec2(0, bh1), True) + if True: + for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): + if imgui.tree_node(f"{cat_name}##b_list"): + if imgui.begin_table(f"bt_{cat_name}", 2): + imgui.table_setup_column("T", imgui.TableColumnFlags_.width_fixed, 220); imgui.table_setup_column("W", imgui.TableColumnFlags_.width_stretch) + for tn in default_tools: + imgui.table_next_row(); imgui.table_next_column(); imgui.text(tn); imgui.table_next_column() + curr_w = app._editing_bias_profile_tool_weights.get(tn, 3); imgui.set_next_item_width(-1) + ch_w, n_w = imgui.slider_int(f"##bw_{tn}", curr_w, 1, 10); + if ch_w: app._editing_bias_profile_tool_weights[tn] = n_w + imgui.end_table() + imgui.tree_pop() + imgui.end_child() + if app._bias_cats_open: + imgui.button("###bias_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): app._bias_split_v = max(0.1, min(0.9, app._bias_split_v + imgui.get_io().mouse_delta.y / rem_bias_y)) + + opened_bc = imgui.collapsing_header("Category Multipliers", imgui.TreeNodeFlags_.default_open) + if opened_bc != app._bias_cats_open: app._bias_cats_open = opened_bc + if app._bias_cats_open: + imgui.begin_child("bcat_scroll", imgui.ImVec2(0, bh2), True) + if True: + if imgui.begin_table("bcats", 2): + imgui.table_setup_column("C", imgui.TableColumnFlags_.width_fixed, 220); imgui.table_setup_column("M", imgui.TableColumnFlags_.width_stretch) + for cn in sorted(models.DEFAULT_TOOL_CATEGORIES.keys()): + imgui.table_next_row(); imgui.table_next_column(); imgui.text(cn); imgui.table_next_column() + curr_m = app._editing_bias_profile_category_multipliers.get(cn, 1.0); imgui.set_next_item_width(-1) + ch_m, n_m = imgui.slider_float(f"##cm_{cn}", curr_m, 0.1, 5.0, "%.1fx"); + if ch_m: app._editing_bias_profile_category_multipliers[cn] = n_m + imgui.end_table() + imgui.end_child() + + if imgui.button("Save Profile", imgui.ImVec2(-1, 0)): + try: + p = models.BiasProfile(name=app._editing_bias_profile_name, tool_weights=app._editing_bias_profile_tool_weights, category_multipliers=app._editing_bias_profile_category_multipliers) + app.controller._cb_save_bias_profile(p, app._editing_tool_preset_scope); app.ai_status = f"Saved: {p.name}" + except Exception as e: app.ai_status = f"Error: {e}" + imgui.end_child() + imgui.end_table() + imgui.end_child() + imgui.end_child() + + # --- Footer Buttons --- + imgui.separator() + if imgui.button("Save##tp", imgui.ImVec2(100, 0)): + if app._editing_tool_preset_name.strip(): app.controller._cb_save_tool_preset(app._editing_tool_preset_name.strip(), app._editing_tool_preset_categories, app._editing_tool_preset_scope); app.ai_status = f"Saved: {app._editing_tool_preset_name}" + imgui.same_line() + if imgui.button("Delete##tp", imgui.ImVec2(100, 0)): + if app._editing_tool_preset_name: app.controller._cb_delete_tool_preset(app._editing_tool_preset_name, app._editing_tool_preset_scope); app._editing_tool_preset_name = ""; app._selected_tool_preset_idx = -1 + imgui.same_line() + if not is_embedded: + if imgui.button("Close##tp", imgui.ImVec2(100, 0)): app.show_tool_preset_manager_window = False + imgui.end_table() + +def render_tool_preset_manager_window(app: App, is_embedded: bool = False) -> None: + if not app.show_tool_preset_manager_window and not is_embedded: return + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) + with imscope.window("Tool Preset Manager", app.show_tool_preset_manager_window) as (opened, visible): + app.show_tool_preset_manager_window = visible + if opened: render_tool_preset_manager_content(app, is_embedded=is_embedded) + else: + render_preset_manager_content(app, is_embedded=is_embedded) + +def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: + if not app.show_persona_editor_window and not is_embedded: return + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) + opened, app.show_persona_editor_window = imgui.begin("Persona Editor", app.show_persona_editor_window) + if not opened: + imgui.end(); return + + if imgui.begin_table("persona_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + # --- Left Sidebar --- + imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row() + + imgui.table_next_column() + imgui.begin_child("persona_list_pane", imgui.ImVec2(0, 0), False) + if True: + if imgui.button("New Persona", imgui.ImVec2(-1, 0)): + app._editing_persona_name = ""; app._editing_persona_system_prompt = "" + app._editing_persona_tool_preset_id = ""; app._editing_persona_bias_profile_id = "" + app._editing_persona_context_preset_id = "" + app._editing_persona_aggregation_strategy = "" + app._editing_persona_preferred_models_list = [{"provider": app.current_provider, "model": app.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}] + app._editing_persona_scope = "project"; app._editing_persona_is_new = True + imgui.separator() + personas = getattr(app.controller, 'personas', {}) + for name in sorted(personas.keys()): + if name and imgui.selectable(f"{name}##p_list", name == app._editing_persona_name and not getattr(app, '_editing_persona_is_new', False))[0]: + p = personas[name]; app._editing_persona_name = p.name; app._editing_persona_system_prompt = p.system_prompt or "" + app._editing_persona_tool_preset_id = p.tool_preset or ""; app._editing_persona_bias_profile_id = p.bias_profile or "" + app._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or "" + app._editing_persona_aggregation_strategy = getattr(p, 'aggregation_strategy', '') or "" + import copy; app._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else [] + app._editing_persona_scope = app.controller.persona_manager.get_persona_scope(p.name); app._editing_persona_is_new = False + imgui.end_child() + + # --- Right Editor --- + imgui.table_next_column() + avail = imgui.get_content_region_avail() + imgui.begin_child("persona_editor_content", imgui.ImVec2(0, avail.y - 45), False) + if True: + header_text = "New Persona" if getattr(app, '_editing_persona_is_new', True) else f"Editing Persona: {app._editing_persona_name}" + imgui.text_colored(C_IN, header_text); imgui.separator() + + if imgui.begin_table("p_meta", 2): + imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 60); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, app._editing_persona_name = imgui.input_text("##pname", app._editing_persona_name, 128) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column() + if imgui.radio_button("Global##pscope", getattr(app, '_editing_persona_scope', 'project') == "global"): app._editing_persona_scope = "global" + imgui.same_line(); + if imgui.radio_button("Project##pscope", getattr(app, '_editing_persona_scope', 'project') == "project"): app._editing_persona_scope = "project" + imgui.end_table() + + rem_y = imgui.get_content_region_avail().y - 100 + if app._persona_models_open and app._persona_prompt_open: h1, h2 = rem_y * app._persona_split_v, rem_y - (rem_y * app._persona_split_v) - 10 + elif app._persona_models_open: h1, h2 = rem_y, 0 + elif app._persona_prompt_open: h1, h2 = 0, rem_y + else: h1, h2 = 0, 0 + + imgui.dummy(imgui.ImVec2(0, 4)) + opened_models = imgui.collapsing_header("Preferred Models", imgui.TreeNodeFlags_.default_open) + if opened_models != app._persona_models_open: app._persona_models_open = opened_models + + if app._persona_models_open: + imgui.begin_child("pref_models_scroll", imgui.ImVec2(0, h1), True) + if True: + to_remove = [] + providers = models.PROVIDERS + if not hasattr(app, '_persona_pref_models_expanded'): app._persona_pref_models_expanded = {} + for i, entry in enumerate(app._editing_persona_preferred_models_list): + imgui.push_id(f"pref_model_{i}") + prov, mod, is_expanded = entry.get("provider", "Unknown"), entry.get("model", "Unknown"), app._persona_pref_models_expanded.get(i, False) + if imgui.button("-" if is_expanded else "+"): app._persona_pref_models_expanded[i] = not is_expanded + imgui.same_line(); imgui.text(f"{i+1}."); imgui.same_line(); imgui.text_colored(C_LBL, f"{prov}"); imgui.same_line(); imgui.text("-"); imgui.same_line(); imgui.text_colored(C_IN, f"{mod}") + if not is_expanded: + imgui.same_line(); summary = f" (T:{entry.get('temperature', 0.7):.1f}, P:{entry.get('top_p', 1.0):.2f}, M:{entry.get('max_output_tokens', 0)})" + imgui.text_colored(C_SUB, summary) + imgui.same_line(imgui.get_content_region_avail().x - 30); + if imgui.button("x"): to_remove.append(i) + if is_expanded: + imgui.indent(20) + if imgui.begin_table("model_settings", 2, imgui.TableFlags_.borders_inner_v): + imgui.table_setup_column("Label", imgui.TableColumnFlags_.width_fixed, 120); imgui.table_setup_column("Control", imgui.TableColumnFlags_.width_stretch) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Provider:"); imgui.table_next_column(); imgui.set_next_item_width(-1) + p_idx = providers.index(prov) + 1 if prov in providers else 0; ch_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers) + if ch_p: entry["provider"] = providers[p_idx-1] if p_idx > 0 else "" + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Model:"); imgui.table_next_column(); imgui.set_next_item_width(-1) + m_list = app.controller.all_available_models.get(entry.get("provider", ""), []); m_idx = m_list.index(mod) + 1 if mod in m_list else 0 + ch_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list) + if ch_m: entry["model"] = m_list[m_idx-1] if m_idx > 0 else "" + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Temperature:"); imgui.table_next_column(); cw = imgui.get_content_region_avail().x + imgui.set_next_item_width(cw * 0.7); _, entry["temperature"] = imgui.slider_float("##ts", entry.get("temperature", 0.7), 0.0, 2.0, "%.1f") + imgui.same_line(); imgui.set_next_item_width(-1); _, entry["temperature"] = imgui.input_float("##ti", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f") + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Top-P:"); imgui.table_next_column() + imgui.set_next_item_width(cw * 0.7); _, entry["top_p"] = imgui.slider_float("##tp_s", entry.get("top_p", 1.0), 0.0, 1.0, "%.2f") + imgui.same_line(); imgui.set_next_item_width(-1); _, entry["top_p"] = imgui.input_float("##tp_i", entry.get("top_p", 1.0), 0.05, 0.05, "%.2f") + imgui.table_next_row(); imgui.table_next_column(); imgui.text("Max Tokens:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096)) + imgui.table_next_row(); imgui.table_next_column(); imgui.text("History Limit:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, entry["history_trunc_limit"] = imgui.input_int("##hist", entry.get("history_trunc_limit", 900000)) + imgui.end_table() + imgui.unindent(20) + imgui.pop_id() + for i in reversed(to_remove): app._editing_persona_preferred_models_list.pop(i) + imgui.end_child() + if app._persona_prompt_open: + imgui.button("###persona_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): app._persona_split_v = max(0.1, min(0.9, app._persona_split_v + imgui.get_io().mouse_delta.y / rem_y)) + + imgui.dummy(imgui.ImVec2(0, 2)) + if imgui.button("Add Preferred Model", imgui.ImVec2(-1, 0)): app._editing_persona_preferred_models_list.append({"provider": app.current_provider, "model": app.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}) + + imgui.dummy(imgui.ImVec2(0, 2)) + if imgui.begin_table("p_assign", 2): + imgui.table_setup_column("C1"); imgui.table_setup_column("C2"); imgui.table_next_row() + imgui.table_next_column(); imgui.text("Tool Preset:"); tn = ["None"] + sorted(app.controller.tool_presets.keys()) + t_idx = tn.index(app._editing_persona_tool_preset_id) if getattr(app, '_editing_persona_tool_preset_id', '') in tn else 0 + imgui.set_next_item_width(-1); _, t_idx = imgui.combo("##ptp", t_idx, tn); app._editing_persona_tool_preset_id = tn[t_idx] if t_idx > 0 else "" + imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(app.controller.bias_profiles.keys()) + b_idx = bn.index(app._editing_persona_bias_profile_id) if getattr(app, '_editing_persona_bias_profile_id', '') in bn else 0 + imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); app._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else "" + imgui.table_next_row() + imgui.table_next_column(); imgui.text("Context Preset:"); cn = ["None"] + sorted(app.controller.project.get("context_presets", {}).keys()) + c_idx = cn.index(app._editing_persona_context_preset_id) if getattr(app, '_editing_persona_context_preset_id', '') in cn else 0 + imgui.set_next_item_width(-1); _, c_idx = imgui.combo("##pcp", c_idx, cn); app._editing_persona_context_preset_id = cn[c_idx] if c_idx > 0 else "" + imgui.table_next_column(); imgui.text("Aggregation Strategy:"); sn = ["auto", "full", "summarize", "skeleton"] + s_idx = sn.index(app._editing_persona_aggregation_strategy) if getattr(app, '_editing_persona_aggregation_strategy', '') in sn else 0 + imgui.set_next_item_width(-1); _, s_idx = imgui.combo("##pas", s_idx, sn); app._editing_persona_aggregation_strategy = sn[s_idx] + imgui.end_table() + + if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): app.show_tool_preset_manager_window = True + + imgui.dummy(imgui.ImVec2(0, 4)); imgui.separator() + opened_prompt = imgui.collapsing_header("System Prompt", imgui.TreeNodeFlags_.default_open) + if opened_prompt != app._persona_prompt_open: app._persona_prompt_open = opened_prompt + + if app._persona_prompt_open: + imgui.begin_child("p_prompt_header_pane", imgui.ImVec2(0, 30), False) + if True: + imgui.text("Template:"); imgui.same_line(); p_pre = ["Select..."] + sorted(app.controller.presets.keys()) + if not hasattr(app, "_load_preset_idx"): app._load_preset_idx = 0 + imgui.set_next_item_width(200); _, app._load_preset_idx = imgui.combo("##load_p", app._load_preset_idx, p_pre) + imgui.same_line(); + if imgui.button("Apply"): + if app._load_preset_idx > 0: app._editing_persona_system_prompt = app.controller.presets[p_pre[app._load_preset_idx]].system_prompt + imgui.same_line(); + if imgui.button("Manage"): app.show_preset_manager_window = True + imgui.end_child() + _, app._editing_persona_system_prompt = imgui.input_text_multiline("##pprompt", app._editing_persona_system_prompt, imgui.ImVec2(-1, h2)) + imgui.end_child() + + # --- Footer Buttons --- + imgui.separator() + if imgui.button("Save##pers", imgui.ImVec2(100, 0)): + if app._editing_persona_name.strip(): + try: + import copy; persona = models.Persona(name=app._editing_persona_name.strip(), system_prompt=app._editing_persona_system_prompt, tool_preset=app._editing_persona_tool_preset_id or None, bias_profile=app._editing_persona_bias_profile_id or None, context_preset=app._editing_persona_context_preset_id or None, aggregation_strategy=app._editing_persona_aggregation_strategy or None, preferred_models=copy.deepcopy(app._editing_persona_preferred_models_list)) + app.controller._cb_save_persona(persona, getattr(app, '_editing_persona_scope', 'project')); app.ai_status = f"Saved: {persona.name}" + except Exception as e: app.ai_status = f"Error: {e}" + else: app.ai_status = "Name required" + imgui.same_line(); + if imgui.button("Delete##pers", imgui.ImVec2(100, 0)): + if not getattr(app, '_editing_persona_is_new', True) and app._editing_persona_name: + app.controller._cb_delete_persona(app._editing_persona_name, getattr(app, '_editing_persona_scope', 'project')) + app._editing_persona_name = ""; app._editing_persona_is_new = True + if not is_embedded: + imgui.same_line() + if imgui.button("Close##pers", imgui.ImVec2(100, 0)): + app.show_persona_editor_window = False + imgui.end_table() + + if not is_embedded: + imgui.end() + +def render_provider_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_provider_panel") + imgui.text("Provider") + if imgui.begin_combo("##prov", app.current_provider): + for p in models.PROVIDERS: + if imgui.selectable(p, p == app.current_provider)[0]: + app.current_provider = p + imgui.end_combo() + imgui.separator() + imgui.text("Model") + if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): + for m in app.available_models: + if imgui.selectable(m, m == app.current_model)[0]: + app.current_model = m + imgui.end_list_box() + imgui.separator() + imgui.text("Parameters") + # Temperature + imgui.push_id("temp") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.temperature = imgui.slider_float("##slider", app.temperature, 0.0, 2.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.temperature = imgui.input_float("Temp", app.temperature, 0.0, 0.0, "%.2f") + imgui.pop_id() + + # Top-P + imgui.push_id("top_p") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.top_p = imgui.slider_float("##slider", app.top_p, 0.0, 1.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.top_p = imgui.input_float("Top-P", app.top_p, 0.0, 0.0, "%.2f") + imgui.pop_id() + + # Max Tokens + imgui.push_id("max_tokens") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.max_tokens = imgui.slider_int("##slider", app.max_tokens, 1, 32768) + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.max_tokens = imgui.input_int("MaxTok", app.max_tokens) + imgui.pop_id() + + ch, app.history_trunc_limit = imgui.input_int("History Truncation Limit", app.history_trunc_limit, 1024) + + if app.current_provider == "gemini_cli": + imgui.separator() + imgui.text("Gemini CLI") + sid = "None" + if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None" + imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200) + if imgui.button("Reset CLI Session"): ai_client.reset_session() + imgui.text("Binary Path") + ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path) + imgui.same_line() + if imgui.button("Browse##gcli"): + r = hide_tk_root() + p = filedialog.askopenfilename(title="Select gemini CLI binary") + r.destroy() + if p: app.ui_gemini_cli_path = p + if ch: + if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: + ai_client._gemini_cli_adapter.binary_path = app.ui_gemini_cli_path + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_provider_panel") + +def render_persona_selector_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_persona_selector_panel") + imgui.text("Persona") + if not hasattr(app, 'ui_active_persona'): app.ui_active_persona = "" + personas = getattr(app.controller, 'personas', {}) + if imgui.begin_combo("##persona", app.ui_active_persona or "None"): + if imgui.selectable("None", not app.ui_active_persona)[0]: app.ui_active_persona = "" + for pname in sorted(personas.keys()): + if not pname: continue + if imgui.selectable(pname, pname == app.ui_active_persona)[0]: + app.ui_active_persona = pname + if pname in personas: + persona = personas[pname] + app._editing_persona_name = persona.name + app._editing_persona_system_prompt = persona.system_prompt or "" + app._editing_persona_tool_preset_id = persona.tool_preset or "" + app._editing_persona_bias_profile_id = persona.bias_profile or "" + app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + app._editing_persona_is_new = False + + # Apply persona to current state immediately + if persona.preferred_models and len(persona.preferred_models) > 0: + first_model = persona.preferred_models[0] + if first_model.get("provider"): + app.current_provider = first_model.get("provider") + if first_model.get("model"): + app.current_model = first_model.get("model") + if first_model.get("temperature") is not None: + ai_client.temperature = first_model.get("temperature") + app.temperature = first_model.get("temperature") + if first_model.get("max_output_tokens"): + ai_client.max_output_tokens = first_model.get("max_output_tokens") + app.max_tokens = first_model.get("max_output_tokens") + if first_model.get("history_trunc_limit"): + app.history_trunc_limit = first_model.get("history_trunc_limit") + + if persona.system_prompt: app.ui_project_system_prompt = persona.system_prompt + if persona.tool_preset: + app.ui_active_tool_preset = persona.tool_preset + ai_client.set_tool_preset(persona.tool_preset) + if persona.bias_profile: + app.ui_active_bias_profile = persona.bias_profile + ai_client.set_bias_profile(persona.bias_profile) + if getattr(persona, 'context_preset', None): + app.ui_active_context_preset = persona.context_preset + app.load_context_preset(persona.context_preset) + imgui.end_combo() + imgui.same_line() + if imgui.button("Manage Personas"): + app.show_persona_editor_window = True + if app.ui_active_persona and app.ui_active_persona in personas: + persona = personas[app.ui_active_persona] + app._editing_persona_name = persona.name + app._editing_persona_system_prompt = persona.system_prompt or "" + app._editing_persona_tool_preset_id = persona.tool_preset or "" + app._editing_persona_bias_profile_id = persona.bias_profile or "" + app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + app._editing_persona_scope = app.controller.persona_manager.get_persona_scope(persona.name) + app._editing_persona_is_new = False + else: + app._editing_persona_name = "" + app._editing_persona_system_prompt = "" + app._editing_persona_tool_preset_id = "" + app._editing_persona_bias_profile_id = "" + app._editing_persona_context_preset_id = "" + app._editing_persona_aggregation_strategy = "" + app._editing_persona_preferred_models_list = [{ + "provider": app.current_provider, + "model": app.current_model, + "temperature": getattr(app, "temperature", 0.7), + "max_output_tokens": getattr(app, "max_tokens", 4096), + "history_trunc_limit": getattr(app, "history_trunc_limit", 900000) + }] + app._editing_persona_scope = "project" + app._editing_persona_is_new = True + imgui.separator() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_persona_selector_panel") + +def render_files_and_media(app: App) -> None: + """ + [C: tests/test_gui_fast_render.py:test_render_files_and_media_fast] + """ + avail = imgui.get_content_region_avail().y + if not hasattr(app, 'files_screenshots_split'): app.files_screenshots_split = 0.65 + split_y = int(avail * app.files_screenshots_split) + if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open): + with imscope.child("Files_child", -1, split_y, True): + if not hasattr(app, 'files_last_selected'): app.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() + + app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower()) + for i, f_item in enumerate(app.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 app.files_last_selected >= 0: + start_i = min(app.files_last_selected, i) + end_i = max(app.files_last_selected, i) + for j in range(start_i, end_i + 1): app.files[j].selected = True + app.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(app, '_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 app.files]: app.files.append(models.FileItem(path=p)) + + imgui.same_line() + if imgui.button("Sel All##selall"): + for f in app.files: + f.selected = True + + imgui.same_line() + if imgui.button("Unsel##unselall"): + for f in app.files: + f.selected = False + + imgui.same_line() + if imgui.button("None##nonesel"): + for f in app.files: + if f.selected: + f.auto_aggregate = False + f.force_full = False + + imgui.same_line() + if imgui.button("Agg##aggsel"): + for f in app.files: + if f.selected: + f.auto_aggregate = True + f.force_full = False + + imgui.same_line() + if imgui.button("Full##fullsel"): + for f in app.files: + if f.selected: + f.force_full = True + f.auto_aggregate = False + + imgui.same_line() + if imgui.button("Del##dels"): + app.files = [f for f in app.files if not f.selected] + + imgui.separator() + + if imgui.collapsing_header("Screenshots", imgui.TreeNodeFlags_.default_open): + with imscope.child("Shots_child", -1, -1, True): + for i, s in enumerate(app.screenshots): + if imgui.button(f"x##s{i}"): + app.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 app.screenshots: app.screenshots.append(p) + return + +def render_files_panel(app: App, height_override: float = 0) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_files_panel") + imgui.text("Paths") + imgui.same_line() + imgui.text("| Base Dir:") + imgui.same_line() + imgui.set_next_item_width(-100) + ch, app.ui_files_base_dir = imgui.input_text("##f_base", app.ui_files_base_dir) + imgui.same_line() + if imgui.button("Browse##fb"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: app.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(app.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(app.files): + imgui.table_next_row() + # Actions + imgui.table_set_column_index(0) + if imgui.button(f"x##f{i}"): + app.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(app, "_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 app.files]: + app.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: app.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"): + app.controller._cb_clear_summary_cache() + + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_files_panel") + +def render_screenshots_panel(app: App, height_override: float = 0) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_screenshots_panel") + imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); + imgui.set_next_item_width(-100) + ch, app.ui_shots_base_dir = imgui.input_text("##s_base", app.ui_shots_base_dir) + imgui.same_line() + if imgui.button("Browse##sb"): + r = hide_tk_root(); d = filedialog.askdirectory(); r.destroy() + if d: app.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 + else: + shot_count = max(len(app.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(app.screenshots): + if imgui.button(f"x##s{i}"): + app.screenshots.pop(i) + break + imgui.same_line(); imgui.text(s) + imgui.end_child() + if imgui.button("Add Screenshot(s)"): + 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: + if p not in app.screenshots: app.screenshots.append(p) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_screenshots_panel") + +def render_context_composition_panel(app: App) -> None: + """ + [C: tests/test_auto_slices.py:test_add_all_triggers_auto_slices, tests/test_gui_fast_render.py:test_render_context_composition_panel_fast] + """ + if imgui.collapsing_header("Context Composition##panel"): + total_lines, total_ast = app._update_context_file_stats() + render_context_batch_actions(app, total_lines, total_ast) + render_context_files_table(app) + + imgui.separator() + if imgui.collapsing_header("Screenshots"): + render_context_screenshots(app) + imgui.separator() + render_context_presets(app) + +def render_ast_inspector_modal(app: App) -> None: + """ + [C: tests/test_ast_inspector_extended.py:test_ast_inspector_line_range_parsing] + """ + if app._show_ast_inspector: + imgui.open_popup('AST Inspector') + app._show_ast_inspector = False + + #region: AST Inspector + + expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) + if opened: + if expanded: + if app.ui_inspecting_ast_file is None: + imgui.close_current_popup() + else: + f_item = app.ui_inspecting_ast_file + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + + if f_path != app._cached_ast_file_path: + outline = "" + try: + if f_path.lower().endswith('.py'): outline = mcp_client.py_get_code_outline(f_path) + elif f_path.lower().endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(f_path) + else: outline = mcp_client.ts_cpp_get_code_outline(f_path) + except Exception as e: + outline = f"Error fetching outline: {e}" + + app._cached_ast_nodes = [] + import re + pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') + stack = [] # (indent, name) + for line in outline.splitlines(): + m = pattern.match(line) + if m: + indent_str, kind, name, start_ln, end_ln = m.groups() + indent = len(indent_str) + while stack and stack[-1][0] >= indent: stack.pop() + stack.append((indent, name)) + full_path = '::'.join([s[1] for s in stack]) + app._cached_ast_nodes.append({ + 'indent': indent, + 'kind': kind, + 'name': name, + 'full_path': full_path, + 'start_line': int(start_ln), + 'end_line': int(end_ln) + }) + try: + content = mcp_client.read_file(f_path) + app._cached_ast_file_lines = content.splitlines() + except Exception: + app._cached_ast_file_lines = ["Error loading file content."] + app._cached_ast_file_path = f_path + + imgui.text(f"Inspecting AST: {f_path}") + imgui.separator() + + #region: ast_dual_pane + if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_next_column() + + #region: LEFT COLUMN (Tree) --- + imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) + if True: + if not app._cached_ast_nodes: imgui.text("No AST nodes found or error fetching outline.") + else: + for node in app._cached_ast_nodes: + indent = node['indent'] + kind = node['kind'] + name = node['name'] + full_path = node['full_path'] + + imgui.dummy(imgui.ImVec2(indent * 10, 0)) + imgui.same_line() + imgui.text(f"[{kind}] {name}") + imgui.same_line(imgui.get_window_width() - 200) + + current_mode = f_item.ast_mask.get(full_path, 'hide') + + imgui.push_id(full_path) + if imgui.radio_button("Def", current_mode == 'def'): f_item.ast_mask[full_path] = 'def' + imgui.same_line() + if imgui.radio_button("Sig", current_mode == 'sig'): f_item.ast_mask[full_path] = 'sig' + imgui.same_line() + if imgui.radio_button("Hide", current_mode == 'hide'): f_item.ast_mask[full_path] = 'hide' + imgui.pop_id() + imgui.end_child() + #endregion: LEFT COLUMN (Tree) + + imgui.table_next_column() + + #region: RIGHT COLUMN (Content) --- + imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) + if True: + if not hasattr(app, '_cached_ast_file_lines') or not app._cached_ast_file_lines: + imgui.text("No file content loaded.") + else: + draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(app._cached_ast_file_lines): + line_num = i + 1 + + # Prioritize the most specific node (deepest indent) that covers the line + deepest_node = None + for node in app._cached_ast_nodes: + if node['start_line'] <= line_num <= node['end_line']: + if deepest_node is None or node['indent'] > deepest_node['indent']: deepest_node = node + + mode = 'hide' + if deepest_node: mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') + + pos = imgui.get_cursor_screen_pos() + line_height = imgui.get_text_line_height() + + if mode == 'def': + # Green, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) + elif mode == 'sig': + # Blue, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + + imgui.text(f"{line_num:4} | {line_text}") + imgui.end_child() + #endregion: RIGHT COLUMN (Content) --- + imgui.end_table() + #endregion: ast_dual_pane + + imgui.separator() + + if imgui.button("Close", imgui.ImVec2(120, 0)): + app.ui_inspecting_ast_file = None + imgui.close_current_popup() + + imgui.end_popup() + + #endregion: AST Inspector + + if not opened: app.ui_inspecting_ast_file = None + +def render_add_context_files_modal(app: App) -> None: + """ + [C: tests/test_auto_slices.py:test_add_selected_triggers_auto_slices] + """ + if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Select files from project to add to context:") + imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) + if True: + # Create a temporary selection set if not initialized + if not hasattr(app, '_ui_picker_selected'): app._ui_picker_selected = set() + for f in app.files: + fpath = f.path if hasattr(f, 'path') else str(f) + # Skip if already in context + if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files): + continue + is_sel = fpath in app._ui_picker_selected + clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) + if clicked: + if new_sel: + app._ui_picker_selected.add(fpath) + else: + app._ui_picker_selected.discard(fpath) + imgui.end_child() + imgui.separator() + + if imgui.button("Add Selected", imgui.ImVec2(120, 0)): + for fpath in app._ui_picker_selected: + f_item = models.FileItem(path=fpath) + app.context_files.append(f_item) + app._populate_auto_slices(f_item) + app._ui_picker_selected.clear() + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + if hasattr(app, '_ui_picker_selected'): + app._ui_picker_selected.clear() + imgui.close_current_popup() + imgui.end_popup() + +def render_save_workspace_profile_modal(app: App) -> None: + if app._show_save_workspace_profile_modal: + imgui.open_popup("Save Workspace Profile") + + if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Name:") + _, app._new_workspace_profile_name = imgui.input_text("##profile_name", app._new_workspace_profile_name) + + imgui.text("Scope:") + if imgui.radio_button("Project", app._new_workspace_profile_scope == "project"): app._new_workspace_profile_scope = "project" + imgui.same_line() + if imgui.radio_button("Global", app._new_workspace_profile_scope == "global"): app._new_workspace_profile_scope = "global" + + imgui.separator() + if imgui.button("Save", (120, 0)): + if app._new_workspace_profile_name.strip(): + app.controller._cb_save_workspace_profile(app._new_workspace_profile_name, app._new_workspace_profile_scope) + app._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", (120, 0)): + app._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.end_popup() + +def render_context_presets_panel(app: App) -> None: + imgui.text_colored(C_IN, "Context Presets") + imgui.separator() + changed, new_name = imgui.input_text("Preset Name##new_ctx", app.ui_new_context_preset_name) + if changed: app.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save Current"): + if app.ui_new_context_preset_name.strip(): + app.save_context_preset(app.ui_new_context_preset_name.strip()) + + imgui.separator() + presets = app.controller.project.get('context_presets', {}) + for name in sorted(presets.keys()): + preset = presets[name] + n_files = len(preset.get('files', [])) + n_shots = len(preset.get('screenshots', [])) + imgui.text(f"{name} ({n_files} files, {n_shots} shots)") + imgui.same_line() + if imgui.button(f"Load##{name}"): app.load_context_preset(name) + imgui.same_line() + if imgui.button(f"Delete##{name}"): app.delete_context_preset(name) + +def render_context_screenshots(app: App) -> None: + for i, s in enumerate(app.screenshots): imgui.text(s) + +def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None: + imgui.text("Batch:") + for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: + if imgui.button(f"{mode.capitalize()}##batch"): + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in app.ui_selected_context_files: f.view_mode = mode + imgui.same_line() + if imgui.button("Sel All##selall"): + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + app.ui_selected_context_files.add(f_path) + imgui.same_line() + if imgui.button("Unsel All##unselall"): app.ui_selected_context_files.clear() + imgui.same_line() + if imgui.button("Add Files"): imgui.open_popup("Select Context Files") + imgui.same_line() + if imgui.button("Add All##addall"): + context_paths = {f.path if hasattr(f, "path") else str(f) for f in app.context_files} + for f in app.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path not in context_paths: + f_copy = copy.deepcopy(f) + app.context_files.append(f_copy) + app._populate_auto_slices(f_copy) + imgui.same_line() + if imgui.button("Del##batch"): + new_files = [] + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path not in app.ui_selected_context_files: new_files.append(f) + app.context_files = new_files + app.ui_selected_context_files.clear() + imgui.same_line() + imgui.text(f" | Total: {len(app.context_files)} files, {total_lines} lines, {total_ast} AST elements") + +def render_context_files_table(app: App) -> None: + imgui.dummy(imgui.ImVec2(0, 4)) + grouped_files = aggregate.group_files_by_dir(app.context_files) + + with imscope.table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders) as active: + if active: + imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_headers_row() + + file_indices = {id(f): idx for idx, f in enumerate(app.context_files)} + + for dir_name, g_files in grouped_files.items(): + imgui.table_next_row() + imgui.table_set_column_index(0) + with imscope.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open) as is_open: + imgui.table_set_column_index(1) + if is_open: + for f_item in g_files: + i = file_indices[id(f_item)] + imgui.table_next_row() + imgui.table_set_column_index(0) + + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + is_sel = f_path in app.ui_selected_context_files + changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) + if changed_sel: + if imgui.get_io().key_shift and app._last_selected_context_index != -1: + start = min(app._last_selected_context_index, i) + end = max(app._last_selected_context_index, i) + for idx in range(start, end + 1): + item = app.context_files[idx] + item_path = item.path if hasattr(item, "path") else str(item) + if is_sel: app.ui_selected_context_files.add(item_path) + else: app.ui_selected_context_files.discard(item_path) + else: + if is_sel: app.ui_selected_context_files.add(f_path) + else: app.ui_selected_context_files.discard(f_path) + app._last_selected_context_index = i + imgui.same_line() + + mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 + cache_key = f"{f_path}_{mtime}" + stats = app._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0}) + f_name = os.path.basename(f_path) + imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})") + + if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): + imgui.same_line() + if imgui.button(f"[Inspect]##{i}"): + app.ui_inspecting_ast_file = f_item + app._show_ast_inspector = True + + imgui.same_line() + if imgui.button(f"[Slices]##{i}"): + app.ui_editing_slices_file = f_item + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + app.text_viewer_title = f"Slices: {f_path}" + try: + app.text_viewer_content = mcp_client.read_file(f_path) + except Exception as e: + app.text_viewer_content = f"Error reading file: {e}" + app.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' + app.show_text_viewer = True + + imgui.table_set_column_index(1) + if not hasattr(f_item, "view_mode"): f_item.view_mode = "summary" + view_modes = ["full", "summary", "skeleton", "outline", "masked", "none"] + try: + current_idx = view_modes.index(f_item.view_mode) + except ValueError: + current_idx = 1 + f_item.view_mode = "summary" + imgui.set_next_item_width(120) + changed_vm, new_idx = imgui.combo(f"##vm{i}", current_idx, view_modes) + if changed_vm: f_item.view_mode = view_modes[new_idx] + + imgui.same_line() + if imgui.button(f"[Save]##vpsave{i}"): imgui.open_popup(f"save_vp_popup{i}") + + if imgui.begin_popup(f"save_vp_popup{i}"): + imgui.text("Preset Name:") + changed_pname, app.ui_new_vp_name = imgui.input_text(f"##pname{i}", app.ui_new_vp_name) + if imgui.button("OK"): + if app.ui_new_vp_name.strip(): + app.controller._cb_save_view_preset(app.ui_new_vp_name.strip(), f_item) + app.ui_new_vp_name = "" + imgui.close_current_popup() + imgui.end_popup() + + imgui.same_line() + if imgui.button(f"[Load]##vpload{i}"): imgui.open_popup(f"load_vp_popup{i}") + + if imgui.begin_popup(f"load_vp_popup{i}"): + vp_names = sorted([vp.name for vp in app.controller.view_presets]) + if not vp_names: imgui.text("No presets saved.") + for vp_name in vp_names: + if imgui.selectable(vp_name): + app.controller._cb_apply_view_preset(vp_name, f_item) + imgui.close_current_popup() + imgui.end_popup() + if hasattr(f_item, "custom_slices") and f_item.custom_slices: + imgui.same_line() + imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") + +def render_context_presets(app: App) -> None: + imgui.text("Presets") + presets = app.controller.project.get('context_presets', {}) + preset_names = [""] + sorted(presets.keys()) + active = getattr(app, "ui_active_context_preset", "") + if active not in preset_names: active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) + if ch: + app.ui_active_context_preset = preset_names[new_idx] + if preset_names[new_idx]: app.load_context_preset(preset_names[new_idx]) + imgui.same_line() + changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", "")) + if changed: app.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save##ctx"): + if getattr(app, "ui_new_context_preset_name", "").strip(): + app.save_context_preset(app.ui_new_context_preset_name.strip()) + app.ui_new_context_preset_name = "" + imgui.same_line() + if imgui.button("Delete##ctx"): + if getattr(app, "ui_active_context_preset", ""): + app.delete_context_preset(app.ui_active_context_preset) + app.ui_active_context_preset = "" + +def render_discussion_hub(app: App) -> None: + with imscope.tab_bar("discussion_hub_tabs"): + with imscope.tab_item("Discussion") as (exp, opened): + if exp: render_discussion_tab(app) + with imscope.tab_item("Context Composition") as (exp, opened): + if exp: render_context_composition_panel(app) + with imscope.tab_item("Snapshot") as (exp, opened): + if exp: render_snapshot_tab(app) + with imscope.tab_item("Takes") as (exp, opened): + if exp: render_takes_panel(app) + return + +def render_discussion_entries(app: App) -> None: + with imscope.child("disc_scroll"): + display_entries = app.disc_entries + if app.ui_focus_agent: + tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: display_entries = [e for e in app.disc_entries if e.get("role") == persona_name or e.get("role") == "User"] + clipper = imgui.ListClipper(); clipper.begin(len(display_entries)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + render_discussion_entry(app, display_entries[i], i) + if app._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); app._scroll_disc_to_bottom = False + +def render_discussion_entry(app: App, entry: dict, index: int) -> None: + with imscope.id(f"disc_{index}"): + collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120) + if imgui.begin_combo("##role", entry["role"]): + for r in app.disc_roles: + if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r + imgui.end_combo() + if not collapsed: + imgui.same_line() + if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode + ts_str = entry.get("ts", "") + if ts_str: + imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str) + if e_dt: + e_unix, next_unix = e_dt.timestamp(), float('inf') + if index + 1 < len(app.disc_entries): + n_ts = app.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts) + if n_dt: next_unix = n_dt.timestamp() + injected = [f for f in app.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix] + if injected: + imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]") + if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected])) + if collapsed: + imgui.same_line() + if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Del"): app.disc_entries.pop(index); return + imgui.same_line() + if imgui.button("Branch"): app._branch_discussion(index) + imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] + if len(entry["content"]) > 60: preview += "..." + if not preview.strip() and entry.get("thinking_segments"): + preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60] + if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..." + imgui.text_colored(vec4(160, 160, 150), preview) + if not collapsed: + thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) + if thinking_segments: render_thinking_trace(app, thinking_segments, index, is_standalone=not has_content) + if read_mode: render_discussion_entry_read_mode(app, entry, index) + else: + if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) + imgui.separator() + +def render_discussion_entry_controls(app: App) -> None: + if imgui.button("+ Entry"): app.disc_entries.append({"role": app.disc_roles[0] if app.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("-All"): + for e in app.disc_entries: e["collapsed"] = True + imgui.same_line() + if imgui.button("+All"): + for e in app.disc_entries: e["collapsed"] = False + imgui.same_line() + if imgui.button("Clear All"): app.disc_entries.clear() + imgui.same_line() + if imgui.button("Save"): app._flush_to_project(); app._flush_to_config(); models.save_config(app.config); app.ai_status = "discussion saved" + _, app.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", app.ui_auto_add_history) + imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80) + ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1) + if app.ui_disc_truncate_pairs < 1: app.ui_disc_truncate_pairs = 1 + imgui.same_line() + if imgui.button("Truncate"): + with app._disc_entries_lock: app.disc_entries = truncate_entries(app.disc_entries, app.ui_disc_truncate_pairs) + app.ai_status = f"history truncated to {app.ui_disc_truncate_pairs} pairs" + +def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None: + content = entry["content"] + if not content.strip(): return + if '## Retrieved Context' in content: + rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) + if rag_match: + rag_section = rag_match.group(1) + if imgui.collapsing_header('Retrieved Context'): + chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) + for chunk_match in chunks: + idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) + if imgui.collapsing_header(f'Chunk {idx}: {path}'): + if imgui.button(f'[Source]##rag_{index}_{idx}'): + res = mcp_client.read_file(path) + if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + imgui.text_unformatted(chunk_content) + content = content[:rag_match.start()] + content[rag_match.end():] + pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") + matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() + if not matches: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'disc_{index}') + else: + with imscope.child(f"read_content_{index}", size_y=150, flags=True): + if app.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + last_idx = 0 + for m_idx, match in enumerate(matches): + before = content[last_idx:match.start()] + if before: + with theme.ai_text_style(): + markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') + header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) + if imgui.collapsing_header(header_text): + if imgui.button(f"[Source]##{index}_{match.start()}"): + res = mcp_client.read_file(path) + if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + if code_block: + with theme.ai_text_style(): + markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') + last_idx = match.end() + after = content[last_idx:] + if after: + with theme.ai_text_style(): + markdown_helper.render(after, context_id=f'disc_{index}_a') + if app.ui_word_wrap: imgui.pop_text_wrap_pos() + +def render_discussion_metadata(app: App) -> None: + disc_data = app.project.get("discussion", {}).get("discussions", {}).get(app.active_discussion, {}) + git_commit, last_updated = disc_data.get("git_commit", ""), disc_data.get("last_updated", "") + imgui.text_colored(C_LBL, "commit:"); imgui.same_line() + render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL)) + imgui.same_line() + if imgui.button("Update Commit"): + if app.ui_project_git_dir: + cmt = project_manager.get_git_commit(app.ui_project_git_dir) + if cmt: disc_data["git_commit"], disc_data["last_updated"], app.ai_status = cmt, project_manager.now_ts(), f"commit: {cmt[:12]}" + imgui.text_colored(C_LBL, "updated:"); imgui.same_line(); imgui.text_colored(C_SUB, last_updated if last_updated else "(never)") + ch, app.ui_disc_new_name_input = imgui.input_text("##new_disc", app.ui_disc_new_name_input); imgui.same_line() + if imgui.button("Create"): + nm = app.ui_disc_new_name_input.strip() + if nm: app._create_discussion(nm); app.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Rename"): + nm = app.ui_disc_new_name_input.strip() + if nm: app._rename_discussion(app.active_discussion, nm); app.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Delete"): app._delete_discussion(app.active_discussion) + +def render_discussion_panel(app: App) -> None: + """ + [C: tests/test_discussion_takes_gui.py:test_render_discussion_tabs, tests/test_discussion_takes_gui.py:test_switching_discussion_via_tabs, tests/test_gui_discussion_tabs.py:test_discussion_tabs_rendered, tests/test_gui_fast_render.py:test_render_discussion_panel_fast, tests/test_gui_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel") + render_thinking_indicator(app) + + if app.is_viewing_prior_session: + render_prior_session_view(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") + return + + render_discussion_selector(app) + + if not app.is_viewing_prior_session: + imgui.separator(); render_discussion_entry_controls(app) + imgui.separator(); render_discussion_roles(app) + imgui.separator(); render_discussion_entries(app) + + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") + return + +def render_discussion_roles(app: App) -> None: + if imgui.collapsing_header("Roles"): + with imscope.child("roles_scroll", size_y=100, flags=True): + for i, r in enumerate(list(app.disc_roles)): + with imscope.id(f"role_{i}"): + if imgui.button("X"): app.disc_roles.pop(i); break + imgui.same_line(); imgui.text(r) + ch, app.ui_disc_new_role_input = imgui.input_text("##new_role", app.ui_disc_new_role_input); imgui.same_line() + if imgui.button("Add"): + r = app.ui_disc_new_role_input.strip() + if r and r not in app.disc_roles: app.disc_roles.append(r); app.ui_disc_new_role_input = "" + return + +def render_discussion_selector(app: App) -> None: + if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): + return + names = app._get_discussion_names(); grouped = {} + for name in names: + base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) + active_base = app.active_discussion.split("_take_")[0] + if active_base not in grouped: active_base = names[0] if names else "" + base_names = sorted(grouped.keys()) + if imgui.begin_combo("##disc_sel", active_base): + for bname in base_names: + is_selected = (bname == active_base) + if imgui.selectable(bname, is_selected)[0]: + target = bname if bname in names else grouped[bname][0] + if target != app.active_discussion: app._switch_discussion(target) + if is_selected: imgui.set_item_default_focus() + imgui.end_combo() + active_base = app.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) + if imgui.begin_tab_bar("discussion_takes_tabs"): + for take_name in current_takes: + label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() + flags = imgui.TabItemFlags_.set_selected if take_name == app.active_discussion else 0 + with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): + if exp and take_name != app.active_discussion: app._switch_discussion(take_name) + with imscope.tab_item("Synthesis###Synthesis") as (exp, _): + if exp: render_synthesis_panel(app) + imgui.end_tab_bar() + if "_take_" in app.active_discussion: + if imgui.button("Promote Take"): + base_name = app.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 + while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 + project_manager.promote_take(app.project, app.active_discussion, new_name); app._switch_discussion(new_name) + imgui.same_line() + if app.active_track: + imgui.same_line(); ch, app._track_discussion_active = imgui.checkbox("Track Discussion", app._track_discussion_active) + if ch: + if app._track_discussion_active: + app._flush_disc_entries_to_project() + history_strings = project_manager.load_track_history(app.active_track.id, app.active_project_root) + with app._disc_entries_lock: app.disc_entries = models.parse_history_entries(history_strings, app.disc_roles) + app.ai_status = f"track discussion: {app.active_track.id}" + else: app._flush_disc_entries_to_project(); app._switch_discussion(app.active_discussion); app.ai_status = "track discussion disabled" + render_discussion_metadata(app) + return + +def render_discussion_tab(app: App) -> None: + imgui.begin_child("HistoryChild", size=(0, -app.ui_discussion_split_h)) + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel") + render_discussion_panel(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") + imgui.end_child() + imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): app.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, app.ui_discussion_split_h - imgui.get_io().mouse_delta.y)) + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, app.ui_separate_message_panel = imgui.checkbox("Pop Out Message", app.ui_separate_message_panel); imgui.same_line() + ch2, app.ui_separate_response_panel = imgui.checkbox("Pop Out Response", app.ui_separate_response_panel) + if ch1: app.show_windows["Message"] = app.ui_separate_message_panel + if ch2: app.show_windows["Response"] = app.ui_separate_response_panel + imgui.pop_style_var() + show_message_tab = not app.ui_separate_message_panel + show_response_tab = not app.ui_separate_response_panel + if show_message_tab or show_response_tab: + if imgui.begin_tab_bar("discussion_tabs"): + tab_flags = imgui.TabItemFlags_.none + if app._autofocus_response_tab: + tab_flags = imgui.TabItemFlags_.set_selected + app._autofocus_response_tab = False + app.controller._autofocus_response_tab = False + if show_message_tab: + if imgui.begin_tab_item("Message", None)[0]: + render_message_panel(app) + imgui.end_tab_item() + if show_response_tab: + if imgui.begin_tab_item("Response", None, tab_flags)[0]: + render_response_panel(app) + imgui.end_tab_item() + imgui.end_tab_bar() + else: + imgui.text_disabled("Message & Response panels are detached.") + +def render_takes_panel(app: App) -> None: + imgui.text("Takes & Synthesis") + imgui.separator() + discussions = app.project.get('discussion', {}).get('discussions', {}) + if not hasattr(app, 'ui_synthesis_selected_takes'): + app.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(app, 'ui_synthesis_prompt'): + app.ui_synthesis_prompt = "" + if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_headers_row() + for name, disc in list(discussions.items()): + imgui.table_next_row() + imgui.table_set_column_index(0) + is_active = name == app.active_discussion + if is_active: + imgui.text_colored(C_IN, name) + else: + imgui.text(name) + imgui.table_set_column_index(1) + history = disc.get('history', []) + imgui.text(f"{len(history)}") + imgui.table_set_column_index(2) + if imgui.button(f"Switch##{name}"): + app._switch_discussion(name) + imgui.same_line() + if name != "main" and imgui.button(f"Delete##{name}"): + del discussions[name] + imgui.end_table() + imgui.separator() + imgui.text("Synthesis") + imgui.text("Select takes to synthesize:") + for name in discussions: + _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + from src import synthesis_formatter + takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + new_name = "synthesis_take" + counter = 1 + while new_name in discussions: + new_name = f"synthesis_take_{counter}" + counter += 1 + app._create_discussion(new_name) + with app._disc_entries_lock: + app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + app._handle_generate_send() + +def render_prior_session_view(app: App) -> None: + with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): + if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True + imgui.separator() + with imscope.child("prior_scroll"): + clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries)) + while clipper.step(): + for idx in range(clipper.display_start, clipper.display_end): + entry = app.prior_disc_entries[idx]; + with imscope.id(f"prior_disc_{idx}"): + collapsed = entry.get("collapsed", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "") + imgui.text_colored(C_LBL, f"[{role}]") + if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts)) + content = entry.get("content", "") + if collapsed: + imgui.same_line(); preview = content.replace("\n", " ")[:80] + if len(content) > 80: preview += "..." + imgui.text_colored(vec4(180, 180, 180), preview) + else: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'prior_disc_{idx}') + imgui.separator() + +def render_thinking_indicator(app: App) -> None: + is_thinking = app.ai_status in ['sending...', 'streaming...', 'running powershell...'] + if is_thinking: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha) + imgui.text_colored(c, "THINKING..."); imgui.same_line() + +def render_message_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_message_panel") +# LIVE indicator + is_live = app.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] + if is_live: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + c = imgui.ImVec4(0.39, 1.0, 0.39, alpha) + if theme.is_nerv_active(): c = vec4(80, 255, 80, alpha) # DATA_GREEN for LIVE in NERV + imgui.text_colored(c, "LIVE") + imgui.separator() + ch, app.ui_ai_input = imgui.input_text_multiline("##ai_in", app.ui_ai_input, imgui.ImVec2(-1, -40)) + # Keyboard shortcuts + io = imgui.get_io() + ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) + if ctrl_l: app.ui_ai_input = "" + imgui.separator() + is_busy = app.ai_status in ['sending...', 'streaming...'] + send_busy = False + with app._send_thread_lock: + if app.send_thread and app.send_thread.is_alive(): send_busy = True + if is_busy: send_busy = True + + imgui.begin_disabled(send_busy) + ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) + label = "Gen + Send (Busy)" if send_busy else "Gen + Send" + if (imgui.button(label) or ctrl_enter) and not send_busy: app._handle_generate_send() + imgui.end_disabled() + imgui.same_line() + if imgui.button("MD Only"): app._handle_md_only() + imgui.same_line() + if imgui.button("Inject File"): app.show_inject_modal = True + imgui.same_line() + if imgui.button("-> History"): + if app.ui_ai_input: app.disc_entries.append({"role": "User", "content": app.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Reset"): app._handle_reset_session() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_message_panel") + +def render_synthesis_panel(app: App) -> None: + """ + + Renders a panel for synthesizing multiple discussion takes. + [C: tests/test_gui_synthesis.py:test_render_synthesis_panel] + """ + imgui.text("Select takes to synthesize:") + discussions = app.project.get('discussion', {}).get('discussions', {}) + if not hasattr(app, 'ui_synthesis_selected_takes'): app.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(app, 'ui_synthesis_prompt'): app.ui_synthesis_prompt = "" + for name in discussions: _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + discussions_dict = app.project.get('discussion', {}).get('discussions', {}) + takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + + new_name = "synthesis_take" + counter = 1 + while new_name in discussions_dict: + new_name = f"synthesis_take_{counter}" + counter += 1 + + app._create_discussion(new_name) + with app._disc_entries_lock: app.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + app._handle_generate_send() + +def render_snapshot_tab(app: App) -> None: + if imgui.begin_tab_bar("snapshot_tabs"): + if imgui.begin_tab_item("Aggregate MD")[0]: + display_md = app.last_aggregate_markdown + if app.ui_focus_agent: + tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: + persona = app.controller.personas.get(persona_name) + if persona and persona.context_preset: + cp_name = persona.context_preset + if cp_name in app._focus_md_cache: + display_md = app._focus_md_cache[cp_name] + else: + flat = src.project_manager.flat_config(app.controller.project, app.active_discussion) + cp = app.controller.project.get('context_presets', {}).get(cp_name) + if cp: + flat["files"]["paths"] = cp.get("files", []) + flat["screenshots"]["paths"] = cp.get("screenshots", []) + full_md, _, _ = src.aggregate.run(flat) + app._focus_md_cache[cp_name] = full_md + display_md = full_md + if imgui.button("Copy"): imgui.set_clipboard_text(display_md) + imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True) + markdown_helper.render(display_md, context_id="snapshot_agg") + imgui.end_child() + imgui.end_tab_item() + if imgui.begin_tab_item("System Prompt")[0]: + if imgui.button("Copy"): imgui.set_clipboard_text(app.last_resolved_system_prompt) + imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) + markdown_helper.render(app.last_resolved_system_prompt, context_id="snapshot_sys") + imgui.end_child() + imgui.end_tab_item() + imgui.end_tab_bar() + +def render_response_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_response_panel") + if app._trigger_blink: + app._trigger_blink = False + app._is_blinking = True + app._blink_start_time = time.time() + try: + imgui.set_window_focus("Response") # type: ignore[call-arg] + except: + pass + is_blinking = False + blink_color = vec4(0, 0, 0, 0) + if app._is_blinking: + elapsed = time.time() - app._blink_start_time + if elapsed > 1.5: + app._is_blinking = False + else: + is_blinking = True + val = math.sin(elapsed * 8 * math.pi) + alpha = 50/255 if val > 0 else 0 + blink_color = vec4(0, 255, 0, alpha) + + with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): + with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): + with imscope.child("response_scroll_area", 0, -40, True): + with theme.ai_text_style(): + segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response) + if segments: + render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], 9999) + markdown_helper.render(parsed_response, context_id="response") + + imgui.separator() + if imgui.button("-> History"): + if app.ai_response: + segments, response = thinking_parser.parse_thinking_trace(app.ai_response) + entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} + if segments: entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] + app.disc_entries.append(entry) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_response_panel") + +def render_operations_hub(app: App) -> None: + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, app.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", app.ui_separate_tool_calls_panel) + if ch1: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel + imgui.same_line() + ch2, app.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", app.ui_separate_usage_analytics) + if ch2: app.show_windows["Usage Analytics"] = app.ui_separate_usage_analytics + imgui.same_line() + ch3, app.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', app.ui_separate_external_tools) + if ch3: app.show_windows['External Tools'] = app.ui_separate_external_tools + imgui.pop_style_var() + show_tc_tab, show_usage_tab = not app.ui_separate_tool_calls_panel, not app.ui_separate_usage_analytics + with imscope.tab_bar("ops_tabs"): + with imscope.tab_item("Comms History") as (exp, _): + if exp: render_comms_history_panel(app) + if show_tc_tab: + with imscope.tab_item("Tool Calls") as (exp, _): + if exp: render_tool_calls_panel(app) + if show_usage_tab: + with imscope.tab_item("Usage Analytics") as (exp, _): + if exp: render_usage_analytics_panel(app) + if not app.ui_separate_external_tools: + with imscope.tab_item("External Tools") as (exp, _): + if exp: + render_external_tools_panel(app) + imgui.separator(); imgui.text("") + try: render_external_editor_panel(app) + except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") + with imscope.tab_item("Workspace Layouts") as (exp, _): + if exp: + imgui.text("Experimental: Auto-switch layout by Tier") + ch, app.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", app.controller.ui_auto_switch_layout) + if app.controller.ui_auto_switch_layout: + imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in app.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = app.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: app.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + +def render_tool_calls_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_calls_panel") + imgui.text("Tool call history") + imgui.same_line() + if imgui.button("Clear##tc"): + app._tool_log.clear() + app._tool_log_dirty = True + imgui.separator() + + log_to_render = app._tool_log_cache + flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y + + if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): + imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) + imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) + + imgui.table_headers_row() + + clipper = imgui.ListClipper() + clipper.begin(len(log_to_render)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + entry = log_to_render[i] + imgui.table_next_row() + + imgui.table_next_column() + imgui.text_colored(C_LBL, f"#{i+1}") + + imgui.table_next_column() + imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") + + imgui.table_next_column() + script = entry.get("script", "") + res = entry.get("result", "") + # Use a clear, formatted combined view for the detail window + combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" + + script_preview = script.replace("\n", " ")[:150] + if len(script) > 150: script_preview += "..." + render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1) + if imgui.is_item_clicked(): + app.text_viewer_title = f"Tool Call #{i+1} Details" + app.text_viewer_content = combined + app.text_viewer_type = 'markdown' + app.show_text_viewer = True + + imgui.table_next_column() + res_preview = res.replace("\n", " ")[:30] + if len(res) > 30: res_preview += "..." + render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1) + if imgui.is_item_clicked(): + app.text_viewer_title = f"Tool Call #{i+1} Details" + app.text_viewer_content = combined + app.text_viewer_type = 'markdown' + app.show_text_viewer = True + + imgui.end_table() + + if app._scroll_tool_calls_to_bottom: + imgui.set_scroll_here_y(1.0) + app._scroll_tool_calls_to_bottom = False + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_calls_panel") + +def render_comms_history_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_comms_history_panel") + st_col = vec4(200, 220, 160) + if theme.is_nerv_active(): st_col = vec4(80, 255, 80) # DATA_GREEN for status in NERV + imgui.text_colored(st_col, f"Status: {app.ai_status}") + imgui.same_line() + if imgui.button("Clear##comms"): + ai_client.clear_comms_log() + app._comms_log.clear() + app._comms_log_dirty = True + if app.is_viewing_prior_session: + imgui.same_line() + if imgui.button("Exit Prior Session"): + app.controller.cb_exit_prior_session() + app._comms_log_dirty = True + imgui.separator() + + imgui.text_colored(C_OUT, "OUT"); imgui.same_line() + imgui.text_colored(C_REQ, "request"); imgui.same_line() + imgui.text_colored(C_TC, "tool_call"); imgui.same_line() + imgui.text(" "); imgui.same_line() + imgui.text_colored(C_IN, "IN"); imgui.same_line() + imgui.text_colored(C_RES, "response"); imgui.same_line() + imgui.text_colored(C_TR, "tool_result") + imgui.separator() + + # Use tinted background for prior session + if app.is_viewing_prior_session: imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) + + imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) + + log_to_render = app._comms_log_cache + + clipper = imgui.ListClipper() + clipper.begin(len(log_to_render)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + entry = log_to_render[i] + imgui.push_id(f"comms_entry_{i}") + + i_display = i + 1 + ts = entry.get("ts", "00:00:00") + direction = entry.get("direction", "??") + kind = entry.get("kind", entry.get("type", "??")) + provider = entry.get("provider", "?") + model = entry.get("model", "?") + tier = entry.get("source_tier", "main") + payload = entry.get("payload", {}) + if not payload and kind not in ("request", "response", "tool_call", "tool_result"): + payload = entry # legacy + + # Row 1: #Idx TS DIR KIND Provider/Model [Tier] + imgui.text_colored(C_LBL, f"#{i_display}"); imgui.same_line() + imgui.text_colored(vec4(160, 160, 160), ts) + + latency = entry.get("latency") or entry.get("metadata", {}).get("latency") + if latency: + imgui.same_line() + imgui.text_colored(C_SUB, f" ({latency:.2f}s)") + + ticket_id = entry.get("mma_ticket_id") + if ticket_id: + imgui.same_line() + imgui.text_colored(vec4(255, 120, 120), f"[{ticket_id}]") + imgui.same_line() + d_col = DIR_COLORS.get(direction, C_VAL) + imgui.text_colored(d_col, direction); imgui.same_line() + k_col = KIND_COLORS.get(kind, C_VAL) + imgui.text_colored(k_col, kind); imgui.same_line() + imgui.text_colored(C_LBL, f"{provider}/{model}"); imgui.same_line() + imgui.text_colored(C_SUB, f"[{tier}]") + + # Optimized content rendering using _render_heavy_text logic + idx_str = str(i) + if kind == "request": + usage = payload.get("usage", {}) + if usage: + inp = usage.get("input_tokens", 0) + imgui.text_colored(C_LBL, f" tokens in:{inp}") + render_heavy_text(app, "message", payload.get("message", ""), idx_str) + if payload.get("system"): + render_heavy_text(app, "system", payload.get("system", ""), idx_str) + elif kind == "response": + r = payload.get("round", 0) + sr = payload.get("stop_reason", "STOP") + usage = payload.get("usage", {}) + usage_str = "" + if usage: + inp = usage.get("input_tokens", 0) + out = usage.get("output_tokens", 0) + cache = usage.get("cache_read_input_tokens", 0) + usage_str = f" in:{inp} out:{out}" + if cache: usage_str += f" cache:{cache}" + imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}") + + text_content = payload.get("text", "") + segments, parsed_response = thinking_parser.parse_thinking_trace(text_content) + if segments: render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) + if parsed_response: render_heavy_text(app, "text", parsed_response, idx_str) + + tcs = payload.get("tool_calls", []) + if tcs: render_heavy_text(app, "tool_calls", json.dumps(tcs, indent=1), idx_str) + + elif kind == "tool_call": render_heavy_text(app, payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) + elif kind == "tool_result": render_heavy_text(app, payload.get("name", "result"), payload.get("output", ""), idx_str) + else: render_heavy_text(app, "data", str(payload), idx_str) + + imgui.separator() + imgui.pop_id() + + if app._scroll_comms_to_bottom: + imgui.set_scroll_here_y(1.0) + app._scroll_comms_to_bottom = False + + imgui.end_child() + if app.is_viewing_prior_session: imgui.pop_style_color() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") + +def render_text_viewer_window(app: App) -> None: + """Renders the standalone text/code/markdown viewer window.""" + if not app.show_text_viewer: return + imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) + expanded, opened = imgui.begin(f"Text Viewer - {app.text_viewer_title}", app.show_text_viewer) + app.show_text_viewer = bool(opened) + if not opened: + app.ui_editing_slices_file = None + app._slice_sel_start = -1 + app._slice_sel_end = -1 + if expanded: + if app.ui_editing_slices_file is not None: + imgui.text_colored(C_IN, "Slice Management (Click-drag lines to select range)") + if imgui.button("Add Selection as Slice"): + if app._slice_sel_start != -1 and app._slice_sel_end != -1: + s_line = min(app._slice_sel_start, app._slice_sel_end) + e_line = max(app._slice_sel_start, app._slice_sel_end) + from src.fuzzy_anchor import FuzzyAnchor + slice_data = FuzzyAnchor.create_slice(app.text_viewer_content, s_line, e_line) + slice_data['tag'] = ""; slice_data['comment'] = "" + app.ui_editing_slices_file.custom_slices.append(slice_data) + app._slice_sel_start = -1; app._slice_sel_end = -1 + imgui.same_line() + if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1 + to_remove = -1 + for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices): + imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() + imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) + if changed_tag: slc['tag'] = new_tag + imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) + if changed_comm: slc['comment'] = new_comm + imgui.same_line() + if imgui.button("Remove"): to_remove = idx + imgui.pop_id() + if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove) + imgui.separator() + if imgui.button("Copy"): imgui.set_clipboard_text(app.text_viewer_content) + imgui.same_line(); _, app.text_viewer_wrap = imgui.checkbox("Word Wrap", app.text_viewer_wrap) + imgui.separator() + renderer = markdown_helper.get_renderer(); tv_type = getattr(app, "text_viewer_type", "text") + if tv_type == 'markdown': + with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(app.text_viewer_content, context_id='text_viewer') + elif app.ui_editing_slices_file is not None: + with imscope.child("slice_editor_content", -1, -1, True): + lines = app.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(lines): + line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height() + is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices) + if is_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2))) + if app._slice_sel_start != -1 and app._slice_sel_end != -1: + s, e = min(app._slice_sel_start, app._slice_sel_end), max(app._slice_sel_start, app._slice_sel_end) + if s <= line_num <= e: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(100, 100, 255, 0.3))) + imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False) + if imgui.is_item_clicked(): app._slice_sel_start = line_num; app._slice_sel_end = line_num + if imgui.is_item_hovered() and imgui.is_mouse_down(0): app._slice_sel_end = line_num + elif tv_type in renderer._lang_map: + if app._text_viewer_editor is None: + app._text_viewer_editor = ced.TextEditor(); app._text_viewer_editor.set_read_only_enabled(True); app._text_viewer_editor.set_show_line_numbers_enabled(True) + try: + app._text_viewer_editor.set_text(app.text_viewer_content) + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_text_viewer_ced") + app._text_viewer_editor.render(f"##ced_{app.text_viewer_title}", imgui.ImVec2(-1, -1)) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_text_viewer_ced") + except Exception as e: imgui.text_colored(vec4(255, 100, 100), f"CED Error: {e}"); imgui.text_unformatted(app.text_viewer_content) + else: + with imscope.child("tv_scroll", -1, -1, True): + if app.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text_unformatted(app.text_viewer_content) + if app.text_viewer_wrap: imgui.pop_text_wrap_pos() + imgui.end() + # Sync text and language + + #region: Inject File Modal + if getattr(app, "show_inject_modal", False): + imgui.open_popup("Inject File") + app.show_inject_modal = False + + if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: + files = app.project.get('files', {}).get('paths', []) + imgui.text("Select File to Inject:") + imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True) + for f_path in files: + is_selected = (app._inject_file_path == f_path) + if imgui.selectable(f_path, is_selected)[0]: + app._inject_file_path = f_path + app.controller._update_inject_preview() + imgui.end_child() + imgui.separator() + if imgui.radio_button("Skeleton", app._inject_mode == "skeleton"): + app._inject_mode = "skeleton" + app.controller._update_inject_preview() + imgui.same_line() + if imgui.radio_button("Full", app._inject_mode == "full"): + app._inject_mode = "full" + app.controller._update_inject_preview() + imgui.separator() + imgui.text("Preview:") + imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(app._inject_preview) + imgui.end_child() + imgui.separator() + if imgui.button("Inject", imgui.ImVec2(120, 0)): + formatted = f"## File: {app._inject_file_path}\n```python\n{app._inject_preview}\n```\n" + with app._disc_entries_lock: + app.disc_entries.append({ + "role": "Context", + "content": formatted, + "collapsed": True, + "ts": project_manager.now_ts() + }) + app._scroll_disc_to_bottom = True + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + imgui.close_current_popup() + imgui.end_popup() + #endregion: Inject File Modal + + render_ast_inspector_modal(app) + return + +def render_base_prompt_diff_modal(app: App) -> None: + if not getattr(app.controller, "_show_base_prompt_diff_modal", False): + return + imgui.open_popup("Base Prompt Diff") + if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt") + imgui.separator() + + default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True) + custom_lines = app.ui_base_system_prompt.splitlines(keepends=True) + diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom')) + + if not diff: + imgui.text("No differences found.") + else: + imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True) + for line in diff: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line.rstrip()) + elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line.rstrip()) + elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line.rstrip()) + else: imgui.text(line.rstrip()) + imgui.end_child() + + imgui.separator() + if imgui.button("Close", imgui.ImVec2(120, 0)): + app.controller._show_base_prompt_diff_modal = False + imgui.close_current_popup() + imgui.end_popup() + +def render_patch_modal(app: App) -> None: + if not app._show_patch_modal: + return + imgui.open_popup("Apply Patch?") + with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + p_min = imgui.get_window_pos() + p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) + shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) + + imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch") + imgui.separator() + if app._pending_patch_files: + imgui.text("Files to modify:") + for f in app._pending_patch_files: imgui.text(f" - {f}") + imgui.separator() + if app._patch_error_message: + imgui.text_colored(vec4(255, 77, 77), f"Error: {app._patch_error_message}") + imgui.separator() + imgui.text("Diff Preview:") + imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) + if app._pending_patch_text: + diff_lines = app._pending_patch_text.split("\n") + for line in diff_lines: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line) + elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line) + elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line) + else: imgui.text(line) + imgui.end_child() + imgui.separator() + if imgui.button("Open in External Editor"): app._open_patch_in_external_editor() + imgui.same_line() + if imgui.button("Apply Patch"): + app._apply_pending_patch() + app._close_vscode_diff() + imgui.same_line() + if imgui.button("Reject"): + app._close_vscode_diff() + app._show_patch_modal = False + app._pending_patch_text = None + app._pending_patch_files = [] + app._patch_error_message = None + imgui.close_current_popup() + +def render_external_editor_panel(app: App) -> None: + from src.external_editor import get_default_launcher + imgui.text("External Editor for Diff Viewing") + imgui.separator() + try: + launcher = get_default_launcher() + editors = launcher.config.editors + default_name = launcher.config.default_editor + if not editors: + imgui.text_colored(C_REQ, " No editors configured") + imgui.text("") + imgui.text("Add editors in config.toml:") + imgui.text(" [tools.text_editors.vscode]") + imgui.text(' path = "C:\\\\path\\\\to\\\\code.exe"') + imgui.text(' diff_args = ["--diff"]') + imgui.text("") + imgui.text(" [tools.text_editors.notepadpp]") + imgui.text(' path = "C:\\\\path\\\\to\\\\notepad++.exe"') + imgui.text(' diff_args = ["-multiInst", "-nosession"]') + imgui.text("") + imgui.text("Then set default in [tools.default_editor]") + else: + imgui.text("Default Editor:") + editor_names = sorted(list(editors.keys())) + if default_name and default_name in editor_names: current_idx = editor_names.index(default_name) + else: current_idx = 0 + changed, new_idx = imgui.combo("##editor_combo", current_idx, editor_names) + if changed: app._set_external_editor_default(editor_names[new_idx]) + imgui.text("") + imgui.text("Configured Editors:") + imgui.separator() + for name in editor_names: + editor = editors.get(name) + if not editor: continue + is_default = name == default_name + marker = " (default)" if is_default else "" + if is_default: imgui.text_colored(C_IN, f" {name}{marker}") + else: imgui.text(f" {name}{marker}") + imgui.text(f" {editor.path}") + if editor.diff_args: imgui.textDisabled(f" diff: {editor.diff_args}") + imgui.text("") + imgui.text("Config: config.toml [tools.text_editors]") + imgui.text("Override: manual_slop.toml default_editor") + except Exception as e: + imgui.text_colored(C_TC, f"Error: {str(e)}") + +def render_approve_script_modal(app: App) -> None: + """Renders the modal dialog for approving AI-generated PowerShell scripts.""" + with app._pending_dialog_lock: + dlg = app._pending_dialog + if dlg: + if not app._pending_dialog_open: + imgui.open_popup("Approve PowerShell Command") + app._pending_dialog_open = True + else: + app._pending_dialog_open = False + + if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not dlg: imgui.close_current_popup() + else: + imgui.text("The AI wants to run the following PowerShell script:") + imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}") + imgui.separator() + # Checkbox to toggle full preview inside modal + _, app.show_text_viewer = imgui.checkbox("Show Full Preview", app.show_text_viewer) + if app.show_text_viewer: + imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(dlg._script) + imgui.end_child() + else: + ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200)) + imgui.separator() + if imgui.button("Approve & Run", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = True + dlg._done = True + dlg._condition.notify_all() + with app._pending_dialog_lock: app._pending_dialog = None + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Reject", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = False + dlg._done = True + dlg._condition.notify_all() + with app._pending_dialog_lock: app._pending_dialog = None + imgui.close_current_popup() + imgui.end_popup() + +def render_markdown_test(app: App) -> None: + imgui.text("Markdown Test Panel") + imgui.separator() + md = """ +# Header 1 +## Header 2 +### Header 3 +This is **bold** text and *italic* text. +And ***bold italic*** text. + +* List item 1 +* List item 2 + * Sub-item + +[Link to Google](https://google.com) + +```python +def hello(): + print("Markdown works!") +``` +""" + markdown_helper.render(md) + +def render_ticket_queue(app: App) -> None: + """ + [C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns] + """ + imgui.text("Ticket Queue Management") + if not app.active_track: + imgui.text_disabled("No active track.") + return + + # Select All / None + if imgui.button("Select All"): app.ui_selected_tickets = {str(t.get('id', '')) for t in app.active_tickets} + imgui.same_line() + if imgui.button("Select None"): app.ui_selected_tickets.clear() + + imgui.same_line(); imgui.spacing(); imgui.same_line() + + # Bulk Actions + if imgui.button("Bulk Execute"): app.bulk_execute() + imgui.same_line() + if imgui.button("Bulk Skip"): app.bulk_skip() + imgui.same_line() + if imgui.button("Bulk Block"): app.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(app.active_tickets): + tid = str(t.get('id', '')) + imgui.table_next_row() + + # Select + imgui.table_next_column() + is_sel = tid in app.ui_selected_tickets + changed, is_sel = imgui.checkbox(f"##sel_{tid}", is_sel) + if changed: + if is_sel: app.ui_selected_tickets.add(tid) + else: app.ui_selected_tickets.discard(tid) + + # ID + imgui.table_next_column() + is_selected = (tid == app.ui_selected_ticket_id) + opened, _ = imgui.selectable(f"{tid}##drag_{tid}", is_selected) + if opened: app.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) + app._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 + app._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; app._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; app._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}"): app._cb_kill_ticket(tid) + elif status == 'todo': + if imgui.button(f"Block##{tid}"): app._cb_block_ticket(tid) + elif status == 'blocked' and t.get('manual_block', False): + if imgui.button(f"Unblock##{tid}"): app._cb_unblock_ticket(tid) + + imgui.end_table() + +def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer + imgui.text("Task DAG") + if (app.active_track or app.active_tickets) and app.node_editor_ctx: + ed.set_current_editor(app.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 app.active_tickets: + if abs(hash(str(t.get('id', '')))) == node_val: + app.ui_selected_ticket_id = str(t.get('id', '')) + break + break + for t in app.active_tickets: + tid = str(t.get('id', '??')) + int_id = abs(hash(tid)) + ed.begin_node(ed.NodeId(int_id)) + if getattr(app, "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 app.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 app.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 app.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) + app._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 app.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] + app._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 app.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"): + app._show_add_ticket_form = not app._show_add_ticket_form + if app._show_add_ticket_form: + # Default Ticket ID + max_id = 0 + for t in app.active_tickets: + tid = t.get('id', '') + if tid.startswith('T-'): + try: max_id = max(max_id, int(tid[2:])) + except: pass + app.ui_new_ticket_id = f"T-{max_id + 1:03d}" + app.ui_new_ticket_desc = "" + app.ui_new_ticket_target = "" + app.ui_new_ticket_deps = "" + if app._show_add_ticket_form: + imgui.begin_child("add_ticket_form", imgui.ImVec2(-1, 220), True) + imgui.text_colored(C_VAL, "New Ticket Details") + _, app.ui_new_ticket_id = imgui.input_text("ID##new_ticket", app.ui_new_ticket_id) + _, app.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", app.ui_new_ticket_desc, imgui.ImVec2(-1, 60)) + _, app.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", app.ui_new_ticket_target) + _, app.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", app.ui_new_ticket_deps) + imgui.text("Priority:") + imgui.same_line() + if imgui.begin_combo("##new_prio", app.ui_new_ticket_priority): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == app.ui_new_ticket_priority)[0]: + app.ui_new_ticket_priority = p_opt + imgui.end_combo() + if imgui.button("Create"): + new_ticket = { + "id": app.ui_new_ticket_id, + "description": app.ui_new_ticket_desc, + "status": "todo", + "priority": app.ui_new_ticket_priority, + "assigned_to": "tier3-worker", + "target_file": app.ui_new_ticket_target, + "depends_on": [d.strip() for d in app.ui_new_ticket_deps.split(",") if d.strip()] + } + app.active_tickets.append(new_ticket) + app._show_add_ticket_form = False + app._push_mma_state_update() + imgui.same_line() + if imgui.button("Cancel"): app._show_add_ticket_form = False + imgui.end_child() + else: + imgui.text_disabled("No active MMA track or tickets.") + +def render_beads_tab(app: App) -> None: + imgui.text("Beads Graph (Dolt-backed)") + if imgui.button("Refresh Beads"): + 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(app, "ui_project_execution_mode", "native") == "beads": + try: + from src import beads_client + bclient = beads_client.BeadsClient(Path(app.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(app: App) -> None: + """ + Main MMA dashboard interface. + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_mma_dashboard") + render_mma_focus_selector(app) + imgui.separator() + if app.is_viewing_prior_session: + c = vec4(255, 152, 48) if theme.is_nerv_active() else vec4(255, 200, 100) + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return + render_mma_track_summary(app) + imgui.separator() + render_mma_epic_planner(app) + imgui.separator() + if imgui.collapsing_header("Conductor Setup"): render_mma_conductor_setup(app) + imgui.separator() + render_mma_track_browser(app) + imgui.separator() + render_mma_global_controls(app) + imgui.separator() + render_mma_usage_section(app) + imgui.separator() + render_ticket_queue(app) + imgui.separator() + app._render_window_if_open("Task DAG", app._render_task_dag_panel, not app.ui_separate_task_dag) + if app.ui_selected_ticket_id: render_mma_ticket_editor(app) + imgui.separator() + render_mma_agent_streams(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + +def render_mma_focus_selector(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Focus Agent:"); imgui.same_line() + focus_label = app.ui_focus_agent or "All" + if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): + if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None + for tier in ["Tier 2", "Tier 3", "Tier 4"]: + if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier + imgui.end_combo() + imgui.same_line() + if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None + +def render_mma_modals(app: App) -> None: + """Renders all MMA-specific approval and info modals.""" + is_nerv = theme.is_nerv_active() + # Tool Execution Approval + if app._pending_ask_dialog: + if not app._ask_dialog_open: + imgui.open_popup("Approve Tool Execution") + app._ask_dialog_open = True + else: + app._ask_dialog_open = False + if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_ask_dialog or app._ask_tool_data is None: imgui.close_current_popup() + else: + tool_name = app._ask_tool_data.get("tool", "unknown"); tool_args = app._ask_tool_data.get("args", {}) + imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator() + imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_approve_ask(); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Deny", imgui.ImVec2(120, 0)): app._handle_reject_ask(); imgui.close_current_popup() + imgui.end_popup() + # MMA Step Approval + if app._pending_mma_approvals: + if not app._mma_approval_open: + imgui.open_popup("MMA Step Approval") + app._mma_approval_open, app._mma_approval_edit_mode = True, False + app._mma_approval_payload = app._pending_mma_approvals[0].get("payload", "") + else: app._mma_approval_open = False + if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_mma_approvals: imgui.close_current_popup() + else: + ticket_id = app._pending_mma_approvals[0].get("ticket_id", "??") + imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval."); imgui.separator() + if app._mma_approval_edit_mode: + imgui.text("Edit Raw Payload (Manual Memory Mutation):"); _, app._mma_approval_payload = imgui.input_text_multiline("##mma_payload", app._mma_approval_payload, imgui.ImVec2(600, 400)) + else: + imgui.text("Proposed Tool Call:"); imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True); imgui.text_unformatted(str(app._pending_mma_approvals[0].get("payload", ""))); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, payload=app._mma_approval_payload); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Payload" if not app._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): app._mma_approval_edit_mode = not app._mma_approval_edit_mode + imgui.same_line() + if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False); imgui.close_current_popup() + imgui.end_popup() + # MMA Spawn Approval + if app._pending_mma_spawns: + if not app._mma_spawn_open: + imgui.open_popup("MMA Spawn Approval") + app._mma_spawn_open, app._mma_spawn_edit_mode = True, False + app._mma_spawn_prompt, app._mma_spawn_context = app._pending_mma_spawns[0].get("prompt", ""), app._pending_mma_spawns[0].get("context_md", "") + else: app._mma_spawn_open = False + if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_mma_spawns: imgui.close_current_popup() + else: + role, ticket_id = app._pending_mma_spawns[0].get("role", "??"), app._pending_mma_spawns[0].get("ticket_id", "??") + imgui.text(f"Spawning {role} for Ticket {ticket_id}"); imgui.separator() + if app._mma_spawn_edit_mode: + imgui.text("Edit Prompt:"); _, app._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", app._mma_spawn_prompt, imgui.ImVec2(800, 200)) + imgui.text("Edit Context MD:"); _, app._mma_spawn_context = imgui.input_text_multiline("##spawn_context", app._mma_spawn_context, imgui.ImVec2(800, 300)) + else: + imgui.text("Proposed Prompt:"); imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True); imgui.text_unformatted(app._mma_spawn_prompt); imgui.end_child() + imgui.text("Proposed Context MD:"); imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True); imgui.text_unformatted(app._mma_spawn_context); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, prompt=app._mma_spawn_prompt, context_md=app._mma_spawn_context); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Mode" if not app._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): app._mma_spawn_edit_mode = not app._mma_spawn_edit_mode + imgui.same_line() + if imgui.button("Abort", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False, abort=True); imgui.close_current_popup() + imgui.end_popup() + # Cycle Detection + if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!") + imgui.text("Please remove the circular dependency.") + if imgui.button("OK"): imgui.close_current_popup() + imgui.end_popup() + +def render_mma_track_summary(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + is_nerv = theme.is_nerv_active() + track_name = app.active_track.description if app.active_track else "None" + if getattr(app, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph" + track_stats = project_manager.calculate_track_progress(app.active_track.tickets if app.active_track else app.active_tickets) + total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in app.mma_tier_usage.values()) + imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL, track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line() + if app.mma_status == "paused": + imgui.text_colored(vec4(255, 152, 48) if is_nerv else imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED"); imgui.same_line() + status_col = imgui.ImVec4(1, 1, 1, 1) + if app.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) + elif app.mma_status == "running": status_col = vec4(80, 255, 80) if is_nerv else imgui.ImVec4(1, 1, 0, 1) + elif app.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) + elif app.mma_status == "error": status_col = vec4(255, 72, 64) if is_nerv else imgui.ImVec4(1, 0, 0, 1) + elif app.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) + imgui.text_colored(status_col, app.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}") + perc = track_stats["percentage"] / 100.0 + p_color = imgui.ImVec4(1, 0, 0, 1) if track_stats["percentage"] < 33 else (imgui.ImVec4(1, 1, 0, 1) if track_stats["percentage"] < 66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_color); imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%"); imgui.pop_style_color() + if imgui.begin_table("ticket_stats_breakdown", 4): + for lbl, val in [("Completed:", track_stats["completed"]), ("In Progress:", track_stats["in_progress"]), ("Blocked:", track_stats["blocked"]), ("Todo:", track_stats["todo"])]: + imgui.table_next_column(); imgui.text_colored(C_LBL, lbl); imgui.same_line(); imgui.text_colored(C_VAL, str(val)) + imgui.end_table() + if app.active_track: + remaining = track_stats["total"] - track_stats["completed"] + eta_mins = (app._avg_ticket_time * remaining) / 60.0 + imgui.text_colored(C_LBL, "ETA:"); imgui.same_line(); imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)") + +def render_mma_epic_planner(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') + _, app.ui_epic_input = imgui.input_text_multiline('##epic_input', app.ui_epic_input, imgui.ImVec2(-1, 80)) + if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): app._cb_plan_epic() + +def render_mma_conductor_setup(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + if imgui.button("Run Setup Scan"): app._cb_run_conductor_setup() + if app.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", app.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only) + +def render_mma_track_browser(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Track Browser") + if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() + for track in app.tracks: + imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column() + status = track.get("status", "unknown").lower() + c = imgui.ImVec4(0.7, 0.7, 0.7, 1) if status == "new" else (vec4(80, 255, 80) if status == "active" and theme.is_nerv_active() else (imgui.ImVec4(1, 1, 0, 1) if status == "active" else (imgui.ImVec4(0, 1, 0, 1) if status == "done" else (imgui.ImVec4(1, 0, 0, 1) if status == "blocked" else imgui.ImVec4(1, 1, 1, 1))))) + imgui.text_colored(c, status.upper()); imgui.table_next_column() + prog = track.get("progress", 0.0) + p_c = imgui.ImVec4(1, 0, 0, 1) if prog < 0.33 else (imgui.ImVec4(1, 1, 0, 1) if prog < 0.66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_c); imgui.progress_bar(prog, imgui.ImVec2(-1, 0), f"{int(prog*100)}%"); imgui.pop_style_color(); imgui.table_next_column() + if imgui.button(f"Load##{track.get('id')}"): app._cb_load_track(str(track.get("id") or "")) + imgui.end_table() + imgui.text("Create New Track") + _, app.ui_new_track_name = imgui.input_text("Name##new_track", app.ui_new_track_name) + _, app.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", app.ui_new_track_desc, imgui.ImVec2(-1, 60)) + imgui.text("Type:"); imgui.same_line() + if imgui.begin_combo("##track_type", app.ui_new_track_type): + for ttype in ["feature", "chore", "fix"]: + if imgui.selectable(ttype, app.ui_new_track_type == ttype)[0]: app.ui_new_track_type = ttype + imgui.end_combo() + if imgui.button("Create Track"): + app._cb_create_track(app.ui_new_track_name, app.ui_new_track_desc, app.ui_new_track_type) + app.ui_new_track_name = ""; app.ui_new_track_desc = "" + +def render_mma_global_controls(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + changed, app.mma_step_mode = imgui.checkbox("Step Mode (HITL)", app.mma_step_mode) + imgui.same_line(); imgui.text(f"Status: {app.mma_status.upper()}") + if app.controller and hasattr(app.controller, 'engine') and app.controller.engine and hasattr(app.controller.engine, '_pause_event'): + imgui.same_line() + is_paused = app.controller.engine._pause_event.is_set() + if imgui.button("Resume" if is_paused else "Pause"): + if is_paused: app.controller.engine.resume() + else: app.controller.engine.pause() + if app.active_tier: + imgui.same_line(); imgui.text_colored(C_VAL, f"| Active: {app.active_tier}") + any_pending = len(app._pending_mma_spawns) > 0 or len(app._pending_mma_approvals) > 0 or app._pending_ask_dialog + if any_pending: + alpha = abs(math.sin(time.time() * 5)) + c = vec4(255, 72, 64, alpha) if theme.is_nerv_active() else imgui.ImVec4(1, 0.3, 0.3, alpha) + imgui.same_line(); imgui.text_colored(c, " APPROVAL PENDING"); imgui.same_line() + if imgui.button("Go to Approval"): pass + imgui.separator() + imgui.text("Hot Reload:") + imgui.same_line() + if imgui.button("Reload GUI"): + success = app._trigger_hot_reload() + if success: + imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Reloaded!") + else: + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error: {app._hot_reload_error or 'Unknown'}") + imgui.same_line(); imgui.text_disabled("(Ctrl+Alt+R)") + +def render_mma_usage_section(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Tier Usage (Tokens & Cost)") + if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): + imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() + total_cost = 0.0 + for tier, stats in app.mma_tier_usage.items(): + imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}") + imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table() + if imgui.collapsing_header("Tier Model Config"): + for tier in app.mma_tier_usage.keys(): + imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = app.mma_tier_usage[tier].get("model", "unknown"), app.mma_tier_usage[tier].get("provider", "gemini") + with imscope.id(f"tier_cfg_{tier}"): + imgui.push_item_width(80) + if imgui.begin_combo("##prov", curr_prov): + for p in models.PROVIDERS: + if imgui.selectable(p, p == curr_prov)[0]: + app.mma_tier_usage[tier]["provider"] = p + models_list = app.controller.all_available_models.get(p, []) + if models_list: app.mma_tier_usage[tier]["model"] = models_list[0] + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + models_list = app.controller.all_available_models.get(curr_prov, []) + if imgui.begin_combo("##model", curr_model): + for m in models_list: + if imgui.selectable(m, curr_model == m)[0]: app.mma_tier_usage[tier]["model"] = m + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1) + curr_preset = app.mma_tier_usage[tier].get("tool_preset") or "None" + p_names = ["None"] + sorted(app.controller.tool_presets.keys()) + if imgui.begin_combo("##preset", curr_preset): + for pn in p_names: + if imgui.selectable(pn, curr_preset == pn)[0]: app.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + curr_pers = app.mma_tier_usage[tier].get("persona") or "None" + personas = getattr(app.controller, 'personas', {}) + pers_opts = ["None"] + sorted(personas.keys()) + if imgui.begin_combo("##persona", curr_pers): + for pern in pers_opts: + if imgui.selectable(pern, curr_pers == pern)[0]: app.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern + imgui.end_combo() + imgui.pop_item_width() + +def render_mma_ticket_editor(app: App) -> None: + imgui.separator(); imgui.text_colored(C_VAL, f"Editing: {app.ui_selected_ticket_id}") + ticket = next((t for t in app.active_tickets if str(t.get('id', '')) == app.ui_selected_ticket_id), None) + if ticket: + imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') + imgui.text("Priority:"); imgui.same_line() + if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; app._push_mma_state_update() + imgui.end_combo() + imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") + personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') + imgui.text("Persona Override:"); imgui.same_line() + pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 + _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) + ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] + if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket['status'] = 'done'; app._push_mma_state_update() + imgui.same_line() + if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): app.active_tickets = [t for t in app.active_tickets if str(t.get('id', '')) != app.ui_selected_ticket_id]; app.ui_selected_ticket_id = None; app._push_mma_state_update() + +def render_mma_agent_streams(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Agent Streams") + if imgui.begin_tab_bar("mma_streams_tabs"): + for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: + with imscope.tab_item(tier) as (exp, _): + if exp: + sep_val = getattr(app, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val) + if ch: + setattr(app, sep_flag_attr, new_val) + app.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val + if not new_val: render_tier_stream_panel(app, tier, label) + else: imgui.text_disabled(f"{tier} stream is detached.") + if getattr(app, "ui_project_execution_mode", "native") == "beads": + with imscope.tab_item("Beads") as (exp, _): + if exp: render_beads_tab(app) + imgui.end_tab_bar() + +def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) -> None: + """ + [C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tier_stream_panel") + if app.is_viewing_prior_session: + imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return + if stream_key is not None: + content = app.mma_streams.get(stream_key, "") + imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) + render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0) + try: + if len(content) != app._tier_stream_last_len.get(stream_key, -1): + imgui.set_scroll_here_y(1.0) + app._tier_stream_last_len[stream_key] = len(content) + imgui.end_child() + except (TypeError, AttributeError): + imgui.end_child() + pass + else: + tier3_keys = [k for k in app.mma_streams if "Tier 3" in k] + if not tier3_keys: + imgui.text_disabled("No worker output yet.") + else: + worker_status = getattr(app, '_worker_status', {}) + for key in tier3_keys: + ticket_id = key.split(": ", 1)[-1] if ": " in key else key + status = worker_status.get(key, "unknown") + if status == "running": + imgui.text_colored(imgui.ImVec4(1, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "completed": + imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "failed": + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"{ticket_id} [{status}]") + else: + imgui.text(f"{ticket_id} [{status}]") + imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) + render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0) + try: + if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1): + imgui.set_scroll_here_y(1.0) + app._tier_stream_last_len[key] = len(app.mma_streams[key]) + imgui.end_child() + except (TypeError, AttributeError): + imgui.end_child() + pass + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + +def render_track_proposal_modal(app: App) -> None: + if app._show_track_proposal_modal: + imgui.open_popup("Track Proposal") + if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]: + from src import shaders + p_min = imgui.get_window_pos() + p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) + # Render soft shadow behind the modal + shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) + + if app._show_track_proposal_modal: + imgui.text_colored(C_IN, "Proposed Implementation Tracks") + imgui.separator() + if not app.proposed_tracks: + imgui.text("No tracks generated.") + else: + for idx, track in enumerate(app.proposed_tracks): + # Title Edit + changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', '')) + if changed_t: + track['title'] = new_t + # Goal Edit + changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60)) + if changed_g: + track['goal'] = new_g + # Buttons + if imgui.button(f"Remove##{idx}"): + app.proposed_tracks.pop(idx) + break + imgui.same_line() + if imgui.button(f"Start This Track##{idx}"): + app._cb_start_track(idx) + imgui.separator() + if imgui.button("Accept", imgui.ImVec2(120, 0)): + app._cb_accept_tracks() + app._show_track_proposal_modal = False + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + app._show_track_proposal_modal = False + imgui.close_current_popup() + else: + imgui.close_current_popup() + imgui.end_popup() + +def render_error_tint(app: App) -> None: + """Renders a red tint overlay if hot reload failed.""" + if not HotReloader.is_error_state: return + draw_list = imgui.get_background_draw_list() + display_size = imgui.get_io().display_size + # Translucent red: (1.0, 0.0, 0.0, 0.2) + tint_col = imgui.get_color_u32(imgui.ImVec4(1.0, 0.0, 0.0, 0.2)) + draw_list.add_rect_filled(imgui.ImVec2(0, 0), display_size, tint_col) + if app.perf_profiling_enabled: + imgui.set_next_window_pos(imgui.ImVec2(10, 50)) + with imscope.window("Hot Reload Error", None, imgui.WindowFlags_.always_auto_resize | imgui.WindowFlags_.no_title_bar): + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), "HOT RELOAD ERROR") + imgui.text_wrapped(HotReloader.last_error or "Unknown error") +