# gui_2.py # defer: parse from __future__ import annotations import copy import datetime import difflib import json import math import os import re import shutil import subprocess import sys import traceback import threading import time import typing # Ensure thirdparty is in sys.path for defer _project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _thirdparty = os.path.join(_project_root, "thirdparty") if _thirdparty not in sys.path: sys.path.insert(0, _thirdparty) from contextlib import ExitStack, nullcontext from pathlib import Path from typing import Optional, Any from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced # Lazy proxies (startup_speedup_20260606 Phase 5D) # -------------------------------------------------------------------------- # These proxy objects replace top-level imports of heavy modules that # should NOT be in the main thread's import chain. The actual import is # deferred until first attribute access (e.g. np.array, filedialog.X, Tk()). # After the first access, the result is cached so subsequent uses are O(1). # This pattern is transparent to call sites: `np.array(...)` and `Tk()` # continue to work unchanged. The savings at startup are 65ms (numpy) + # stdlib tkinter (variable; not on the original baseline but still real). # -------------------------------------------------------------------------- import importlib as _importlib from typing import Any as _Any from typing import Optional as _Optional #TODO(Ed): Remove Excpetion based errors class _LazyModule: """Lazy proxy that defers an import until first attribute access or call. Use as a module-level name to replace a top-level import. The wrapped module is loaded once and cached. Supports both attribute access (e.g. np.array) and calling (e.g. Tk()). """ def __init__(self, module_name: str, attr_name: _Optional[str] = None) -> None: self._module_name = module_name self._attr_name = attr_name self._cached: _Optional[_Any] = None def _resolve(self) -> _Any: if self._cached is None: mod = _importlib.import_module(self._module_name) if self._attr_name is None: self._cached = mod else: try: self._cached = getattr(mod, self._attr_name) except AttributeError: sub_mod_name = f"{self._module_name}.{self._attr_name}" try: self._cached = _importlib.import_module(sub_mod_name) except (ImportError, ModuleNotFoundError): self._cached = _FiledialogStub() return self._cached def __getattr__(self, name: str) -> _Any: return getattr(self._resolve(), name) def __call__(self, *args: _Any, **kwargs: _Any) -> _Any: return self._resolve()(*args, **kwargs) class _FiledialogStub: """No-op replacement for tkinter.filedialog on Python installs where the Tcl/Tk runtime is missing (e.g. embedded Python, slim Docker images). All dialog functions return safe empty sentinels so call sites that do `if p and p not in app.x: app.x.append(p)` treat a missing dialog as a no-op. Exposes a `available` flag so the UI can detect the stub and offer an ImGui-based path input as an alternative. """ available: bool = False def askopenfilename(self, *args: _Any, **kwargs: _Any) -> str: return "" def askopenfilenames(self, *args: _Any, **kwargs: _Any) -> tuple: return () def askdirectory(self, *args: _Any, **kwargs: _Any) -> str: return "" def asksaveasfilename(self, *args: _Any, **kwargs: _Any) -> str: return "" # Heavy modules that were previously top-level imports (now lazy): np = _LazyModule("numpy") # was: import numpy as np filedialog = _LazyModule("tkinter", "filedialog") # was: from tkinter import filedialog Tk = _LazyModule("tkinter", "Tk") # was: from tkinter import Tk from src.diff_viewer import apply_patch_to_file from src import ai_client from src import aggregate from src import api_hooks from src import app_controller from src import bg_shader from src import cost_tracker from src import history from src import imgui_scopes as imscope from src import paths # from src import presets from src import project_manager from src import session_logger from src import log_registry # from src import log_pruner from src import models from src.models import GenerateRequest, ConfirmRequest from src import mcp_client from src import markdown_helper from src import shaders from src import synthesis_formatter from src import theme_2 as theme from src import thinking_parser from src import workspace_manager from src.hot_reloader import HotReloader win32gui: Any = None win32con: Any = None COMMS_CLAMP_CHARS: int = 300 def hide_tk_root() -> Tk: root = Tk() root.withdraw() root.wm_attributes("-topmost", True) return root # Standard Color Constants (now bound to the theming system) def C_OUT() -> imgui.ImVec4: return theme.get_color("status_info") def C_IN() -> imgui.ImVec4: return theme.get_color("status_success") def C_REQ() -> imgui.ImVec4: return theme.get_color("status_warning") def C_RES() -> imgui.ImVec4: return theme.get_color("status_success") def C_TC() -> imgui.ImVec4: return theme.get_color("status_warning") def C_TR() -> imgui.ImVec4: return theme.get_color("status_info") def C_TRS() -> imgui.ImVec4: return theme.get_color("status_info") def C_LBL() -> imgui.ImVec4: return theme.get_color("text_disabled") def C_VAL() -> imgui.ImVec4: return theme.get_color("text") def C_KEY() -> imgui.ImVec4: return theme.get_color("status_info") def C_NUM() -> imgui.ImVec4: return theme.get_color("status_success") def C_TRM() -> imgui.ImVec4: return theme.get_color("text_disabled") def C_SUB() -> imgui.ImVec4: return theme.get_color("text_disabled") DIR_COLORS: dict[str, typing.Callable[[], imgui.ImVec4]] = {"OUT": C_OUT, "IN": C_IN} KIND_COLORS: dict[str, typing.Callable[[], imgui.ImVec4]] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS} HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"} def render_text_viewer(app: App, label: str, content: str, text_type: str = 'text', force_open: bool = False, id_suffix: str = "") -> None: if imgui.button(f"[+]##{id_suffix or str(id(content))}") or force_open: app.text_viewer_type = text_type app.show_windows["Text Viewer"] = True app.text_viewer_title = label app.text_viewer_content = content def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Any = None) -> None: with imscope.id(label + str(hash(value))): with imscope.style_color(imgui.Col_.frame_bg, imgui.ImVec4(0, 0, 0, 0)), \ imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0): if color: with imscope.style_color(imgui.Col_.text, color): if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only) else: if multiline: _, _ = imgui.input_text_multiline(f"##{label}", value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) else: _, _ = imgui.input_text(f"##{label}", value, imgui.InputTextFlags_.read_only) def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict[str, Any]]: if max_pairs <= 0: return [] count, target = 0, max_pairs * 2 for i in range(len(entries) - 1, -1, -1): if entries[i].get("role", "") in ("User", "AI"): count += 1 if count == target: return entries[i:] return entries def _detect_refresh_rate_win32_result() -> Result[float]: """Drain-aware variant of _detect_refresh_rate_win32 (L216 INTERNAL_SILENT_SWALLOW). Extracts the thirdparty ctypes user32.EnumDisplaySettingsW try/except from _detect_refresh_rate_win32 into a Result-returning helper. On exception, returns Result(data=0.0, errors=[ErrorInfo]) so the legacy wrapper can fall back to the safe 0.0 default. On success, returns Result(data=rate) where rate is the detected display frequency in Hz. [C: src/gui_2.py:_detect_refresh_rate_win32 (L216 legacy wrapper)] """ #Note(Ed): Exception(Thirdparty) try: import ctypes from ctypes import wintypes class _DEVMODE(ctypes.Structure): _fields_ = [ ("dmDeviceName", wintypes.WCHAR * 32), ("dmSpecVersion", wintypes.WORD), ("dmDriverVersion", wintypes.WORD), ("dmSize", wintypes.WORD), ("dmDriverExtra", wintypes.WORD), ("dmFields", wintypes.DWORD), ("dmStuff", ctypes.c_byte * 16), ("dmColor", wintypes.SHORT), ("dmDuplex", wintypes.SHORT), ("dmYResolution", wintypes.SHORT), ("dmTTOption", wintypes.SHORT), ("dmCollate", wintypes.SHORT), ("dmFormName", wintypes.WCHAR * 32), ("dmLogPixels", wintypes.WORD), ("dmBitsPerPel", wintypes.DWORD), ("dmPelsWidth", wintypes.DWORD), ("dmPelsHeight", wintypes.DWORD), ("dmDisplayFlags", wintypes.DWORD), ("dmDisplayFrequency", wintypes.DWORD), ("dmICMMethod", wintypes.DWORD), ("dmICMIntent", wintypes.DWORD), ("dmMediaType", wintypes.DWORD), ("dmDitherType", wintypes.DWORD), ("dmReserved1", wintypes.DWORD), ("dmReserved2", wintypes.DWORD), ("dmPanningWidth", wintypes.DWORD), ("dmPanningHeight", wintypes.DWORD), ] dm = _DEVMODE() dm.dmSize = ctypes.sizeof(_DEVMODE) if ctypes.windll.user32.EnumDisplaySettingsW(None, -1, ctypes.byref(dm)): if dm.dmDisplayFrequency > 1: return Result(data=float(dm.dmDisplayFrequency)) return Result(data=0.0) except Exception as e: return Result(data=0.0, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"win32 EnumDisplaySettingsW failed: {e}", source="gui_2._detect_refresh_rate_win32_result", original=e, )]) def _detect_refresh_rate_win32() -> float: """Return the primary display's current refresh rate in Hz, or 0.0 on failure. Uses user32.EnumDisplaySettingsW (ENUM_CURRENT_SETTINGS) which reads the value directly from the display driver in microseconds. The previous implementation shelled out to PowerShell + WMI (Get-CimInstance Win32_VideoController), which cost ~350ms on every startup and blocked the first frame. Legacy wrapper: delegates to _detect_refresh_rate_win32_result. Preserves the original signature (returns float). The call site in App.__init__ invokes the result helper directly and drains errors to self._startup_timeline_errors. """ result = _detect_refresh_rate_win32_result() return result.data def _resolve_font_path_result(font_path: str, assets_dir: Path) -> Result[str]: """Drain-aware variant of _resolve_font_path (L264 INTERNAL_SILENT_SWALLOW). Extracts the multi-step normalization logic into a Result-returning helper. The narrow except (ValueError, AttributeError) at the is_relative_to check is converted to a full Result conversion (logging NOT a drain per the user's principle 2026-06-17). On success, returns Result(data=resolved_path). On exception at is_relative_to, returns Result(data="fonts/Inter-Regular.ttf", errors=[ErrorInfo]) so the legacy wrapper can fall back to the bundled Inter font (preserving the original behavior). [C: src/gui_2.py:_resolve_font_path (L264 legacy wrapper)] """ p = Path(font_path) if not p.is_absolute(): return Result(data=font_path) try: if p.is_relative_to(assets_dir): return Result(data=str(p.relative_to(assets_dir)).replace("\\", "/")) except Exception as e: return Result(data="fonts/Inter-Regular.ttf", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Path.is_relative_to failed during font path resolution: {e}", source="gui_2._resolve_font_path_result", original=e, )]) if p.exists(): return Result(data=font_path) name = p.name for rel in (f"fonts/{name}", name): if (assets_dir / rel).exists(): return Result(data=rel) return Result(data="fonts/Inter-Regular.ttf") def _resolve_font_path(font_path: str, assets_dir: Path) -> str: """Normalize a configured font path to something hello_imgui can load. hello_imgui resolves relative paths against the assets folder. A config may carry a stale ABSOLUTE path from a different project checkout (e.g. C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf after the repo moved to C:/projects/sloppy). In that case the absolute file does not exist and the load fails. This recovers by: 1. If the absolute path lives under the current assets folder -> relativize. 2. If the absolute path exists on disk as-is -> keep it. 3. Otherwise recover the basename under assets/fonts or assets. 4. Final fallback: the bundled default Inter font. Legacy wrapper: delegates to _resolve_font_path_result. Preserves the original signature (returns str). The call site in App._load_fonts invokes the result helper directly and drains errors to self._startup_timeline_errors. """ return _resolve_font_path_result(font_path, assets_dir).data def _apply_runtime_caps_override(app: "App", caps: "VendorCapabilities") -> "VendorCapabilities": from dataclasses import replace if app.current_provider == "llama": from src import ai_client base_url: str = getattr(ai_client, "_llama_base_url", "") if "localhost" in base_url or "127.0.0.1" in base_url: return replace(caps, local=True) return caps def _render_v2_capability_badges(caps: "VendorCapabilities") -> None: """Render small colored badges for the 11 v2 capability flags. Only fields where caps. is True are shown. Each badge has a tooltip with the field name. Per-field colors map to the existing theme convention: green for supported, grey for not. Fields with no entry (False) are silently omitted. Added 2026-06-11 as part of Phase 5 t5_4 (UI adaptations for new v2 fields). The 11 fields are the v2 matrix fields beyond the original 7 v1 fields (vision, tool_calling, caching, streaming, model_discovery, context_window, cost_tracking) which are already gated elsewhere in the GUI. """ badged_fields: list[tuple[str, str]] = [ ("reasoning", "Reasoning"), ("structured_output", "JSON"), ("code_execution", "Code"), ("web_search", "Web"), ("x_search", "X"), ("file_search", "File"), ("mcp_support", "MCP"), ("audio", "Audio"), ("video", "Video"), ("grounding", "Ground"), ("computer_use", "Comp"), ] enabled: list[tuple[str, str]] = [] for field_name, label in badged_fields: if getattr(caps, field_name, False): enabled.append((field_name, label)) if not enabled: return imgui.text("Capabilities") for field_name, label in enabled: imgui.same_line(); imgui.text_colored(theme.get_color("status_success"), f" [{label}]") if imgui.is_item_hovered(): imgui.set_tooltip(f"caps.{field_name}=True") class App: """The main ImGui interface orchestrator for Manual Slop.""" def __init__(self) -> None: """Initializes core app dependencies (controller, history, performance monitor, command palette, workspace manager) and registers app callback handlers. SSDL Shape: `[I:init_controller] -> [I:init_workspace] -> [I:load_profiles]` """ #region: --- Core Dependencies & State --- from src.startup_profiler import startup_profiler with startup_profiler.phase("app_init_AppController"): self.controller = app_controller.AppController(defer_warmup=True) self.controller._app = self with startup_profiler.phase("app_init_history_perfmon"): from src import history, performance_monitor self.perf_monitor = performance_monitor.PerformanceMonitor() self.history = history.HistoryManager(max_capacity=100) # --- Undo/Redo & Snapshot State --- self._last_ui_snapshot: Optional[history.UISnapshot] = None self._snapshot_timer: float = 0.0 self._snapshot_debounce: float = 1.5 self._pending_snapshot: bool = False self._is_applying_snapshot: bool = False # --- Command Palette --- self.show_command_palette: bool = False # --- Initialization --- with startup_profiler.phase("app_init_state"): self.controller.init_state() from src.hot_reloader import HotReloader, HotModule if 'src.gui_2' not in HotReloader.HOT_MODULES: HotReloader.register(HotModule( name = 'src.gui_2', file_path = __file__, state_keys = ['active_discussion', 'show_windows', 'ui_file_paths', 'ui_screenshot_paths', 'disc_entries', 'disc_roles'], delegation_targets = ['_render_main_interface', '_render_discussion_hub', '_render_files_and_media', '_render_ai_settings_hub', '_render_operations_hub', '_render_mma_dashboard'] )) with startup_profiler.phase("app_init_workspace"): self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root) self.disc_entries = self.controller.disc_entries self.disc_roles = self.controller.disc_roles self.workspace_profiles = self.workspace_manager.load_all_profiles() with startup_profiler.phase("app_init_start_services"): self.controller.start_services(self) #region: --- Controller Callbacks & Actions --- self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset self.controller._predefined_callbacks['delete_context_preset'] = self.delete_context_preset self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p) self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p) self.controller._predefined_callbacks['set_context_files_for_test'] = lambda files: setattr(self, 'context_files', [models.FileItem(path=f) for f in files]) self.controller._predefined_callbacks['set_screenshots_for_test'] = lambda ss: setattr(self, 'screenshots', ss) self.controller._predefined_callbacks['_toggle_command_palette'] = self._toggle_command_palette self.controller._gettable_fields['show_command_palette'] = 'show_command_palette' #endregion: --- Core Dependencies & State --- def _save_context_preset_force(name: str): if not name: return preset_files = [] for f in self.context_files: p = f.path if hasattr(f, 'path') else str(f) vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn)) preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots)) self.controller.save_context_preset(preset) self.ui_new_context_preset_name = "" self.show_missing_files_modal = False self.controller._predefined_callbacks['get_app_debug_info'] = lambda: self.app_debug_info self.controller._gettable_fields ['app_debug_info'] = 'app_debug_info' self.controller._predefined_callbacks['save_context_preset_force'] = _save_context_preset_force self.controller._predefined_callbacks['set_ui_attr'] = lambda k, v: setattr(self, k, v) self.controller._predefined_callbacks['set_context_files'] = self._set_context_files self.controller._predefined_callbacks['simulate_save_preset'] = self._simulate_save_preset self.controller._clickable_actions.update({ 'btn_undo': self._handle_undo, 'btn_redo': self._handle_redo, 'btn_open_external_editor': self._open_patch_in_external_editor, 'Save##ctx': self._handle_save_ctx_click, 'Save Anyway': self._handle_save_anyway_click, }) #region: --- UI Component State --- self.text_viewer_wrap = True self._text_viewer_editor: Optional[ced.TextEditor] = None self.show_windows.setdefault("Diagnostics", False) self.show_windows.setdefault("Usage Analytics", False) self.show_windows.setdefault("Context Preview", False) self.show_windows.setdefault("Tier 1: Strategy", False) self.show_windows.setdefault("Tier 2: Tech Lead", False) self.show_windows.setdefault("Tier 3: Workers", False) self.show_windows.setdefault("Tier 4: QA", False) self.show_windows.setdefault('External Tools', False) self.show_windows.setdefault('Shader Editor', False) self.show_windows.setdefault('Undo/Redo History', False) #endregion: --- UI Component State --- #region: --- Preset & Profile Management State --- self.context_preview_text = "" self.ui_separate_context_preview = False self.ui_active_context_preset = "" self.target_context_preset_name = "" self.show_empty_context_modal = False self._pending_generation_action = None # 'generate' or 'md_only' self._pending_save_ctx_click = False self._pending_save_anyway_click = False self.show_missing_files_modal = False self.show_structural_editor_modal = False self.show_command_palette = False self.ui_command_search = "" self.ui_command_idx = 0 self.missing_context_files = [] self._new_preset_name = "" self._editing_preset_name = "" self._editing_preset_system_prompt = "" self._editing_preset_temperature = 0.0 self._editing_preset_top_p = 1.0 self._editing_preset_max_output_tokens = 4096 self._editing_preset_scope = "project" self._editing_preset_is_new = False self._presets_list: dict[str, dict] = {} self._selected_preset_idx = -1 self._show_save_preset_modal = False self._editing_tool_preset_name = '' self._editing_tool_preset_categories = {} self._editing_tool_preset_scope = 'project' self._selected_tool_preset_idx = -1 self._editing_bias_profile_name = "" self._editing_bias_profile_tool_weights: dict[str, int] = {} self._editing_bias_profile_category_multipliers: dict[str, float] = {} self._editing_bias_profile_scope = "project" self._selected_bias_profile_idx = -1 self._new_bias_tool_name = "run_powershell" self._new_bias_category_name = "General" self._editing_persona_name = "" self._editing_persona_description = "" self._editing_persona_provider = "" self._editing_persona_model = "" self._editing_persona_system_prompt = "" self._editing_persona_temperature = 0.7 self._editing_persona_top_p = 1.0 self._editing_persona_max_tokens = 4096 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 : list[dict] = [] self._editing_persona_scope = "project" self._editing_persona_is_new = True self._persona_editor_opened = False self._personas_list: dict[str, dict] = {} #endregion --- Preset & Profile Management State --- # --- Layout State --- self._persona_split_v = 0.4 self._persona_models_open = True self._persona_prompt_open = True self._tool_split_v = 0.4 self._bias_split_v = 0.6 self._tool_list_open = True self._bias_list_open = True self._bias_weights_open = True self._bias_cats_open = True self._prompt_md_preview = False self.ui_discussion_split_h = 300.0 # --- Workspace State --- gui_cfg = self.config.get("gui", {}) self.ui_multi_viewport = gui_cfg.get("multi_viewport", False) self.layout_presets = self.config.get("layout_presets", {}) self._show_save_workspace_profile_modal = False self._new_workspace_profile_name = "" self._new_workspace_profile_scope = "project" self.ui_new_vp_name = "" # --- Controller Lock Aliases --- self._send_thread_lock = self.controller._send_thread_lock self._disc_entries_lock = self.controller._disc_entries_lock self._pending_comms_lock = self.controller._pending_comms_lock self._pending_tool_calls_lock = self.controller._pending_tool_calls_lock self._pending_history_adds_lock = self.controller._pending_history_adds_lock self._pending_gui_tasks_lock = self.controller._pending_gui_tasks_lock self._pending_dialog_lock = self.controller._pending_dialog_lock self._api_event_queue_lock = self.controller._api_event_queue_lock # --- Cache & Discussion State --- self._discussion_names_cache: list[str] = [] self._discussion_names_dirty: bool = True self._comms_log_cache: list[dict[str, Any]] = [] self._tool_log_cache: list[dict[str, Any]] = [] self._focus_md_cache: dict[str, str] = {} self._last_ui_focus_agent: Optional[str] = None self._log_registry: Optional[log_registry.LogRegistry] = None # --- Node Editor State --- self.node_editor_config = ed.Config() self.node_editor_ctx = ed.create_editor(self.node_editor_config) # --- Context & AST State --- self.ui_selected_ticket_id: Optional[str] = None self.ui_selected_tickets: set[str] = set() self.ui_selected_context_files: set[str] = set() self.ui_new_ticket_priority: str = 'medium' self._autofocus_response_tab = False self._last_selected_context_index = -1 self.ui_inspecting_ast_file = None self._show_ast_inspector = False self._cached_ast_nodes = [] self._cached_ast_file_path = '' self._cached_ast_file_lines = [] self.ui_editing_slices_file = None self._slice_sel_start = -1 self._slice_sel_end = -1 self.context_files = [] self.ui_synthesis_prompt: str = "" self.ui_synthesis_selected_takes: dict[str, bool] = {} # --- Rendering & Theme State --- self.perf_show_graphs:dict[str, bool] = {} self.ui_crt_filter = False self.ui_tool_filter_category = "All" self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8} self._is_generating_preview = False self._pending_preview_refresh = False self._preview_refresh_timer: float = 0.0 self._preview_refresh_debounce: float = 0.5 self._hot_reload_error: Optional[str] = None def _set_context_files(self, paths: list[str]) -> None: from src import models self.context_files = [models.FileItem(path=p) for p in paths] self.controller.context_files = self.context_files def _simulate_save_preset(self, name: str) -> None: from src import models item = models.FileItem(path='test.py') self.files = [item] self.context_files = [item] self.screenshots = ['test.png'] self.save_context_preset(name) def _handle_approve_ask(self) -> None: """UI-level wrapper for approving a pending tool execution ask.""" self.controller._handle_approve_ask() def _handle_save_ctx_click(self) -> None: self._pending_save_ctx_click = True def _handle_save_anyway_click(self) -> None: self._pending_save_anyway_click = True def _post_init(self) -> None: theme.apply_current() # Register warmup completion callback (sub-track 4 of # startup_speedup_20260606). The callback runs on a background # _io_pool thread; it only sets primitive state on the App, which # is safe. The render_warmup_status_indicator() function reads # the timestamp to show a brief "ready" tag for 3 seconds. if hasattr(self.controller, "on_warmup_complete"): cb_result = _post_init_callback_result(self) if not cb_result.ok: if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("_post_init.callback", cb_result.errors[0])) self._diag_layout_state() def _diag_layout_state(self) -> None: """One-shot startup diagnostic: log show_windows state and warn if the on-disk manualslop_layout.ini references window names that no longer exist in the current code. Helps users and test operators detect stale layout state at a glance instead of debugging missing panels. """ import os as _os visible_by_default = [w for w, v in self.show_windows.items() if v] sys.stderr.write(f"[GUI] show_windows entries: {len(self.show_windows)}, visible by default: {len(visible_by_default)}\n") if visible_by_default: sys.stderr.write(f"[GUI] visible-by-default windows: {', '.join(sorted(visible_by_default))}\n") ini_path = _os.path.join(_os.getcwd(), "manualslop_layout.ini") if not _os.path.exists(ini_path): sys.stderr.write(f"[GUI] no layout file at {ini_path} (HelloImGui will create a fresh one on shutdown)\n") return ini_size = _os.path.getsize(ini_path) sys.stderr.write(f"[GUI] layout file: {ini_path} ({ini_size} bytes)\n") result = _diag_layout_state_ini_text_result(self, ini_path) if not result.ok: if not hasattr(self, "_startup_timeline_errors"): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("_diag_layout_state", result.errors[0])) return _ini_text = result.data _STALE_WINDOW_NAMES = { "Projects", "Files", "Screenshots", "Discussion History", "Provider", "Message", "Response", "Tool Calls", "Comms History", "System Prompts", } _stale_found = [n for n in _STALE_WINDOW_NAMES if f"[Window][{n}]" in _ini_text] if _stale_found: sys.stderr.write(f"[GUI] WARNING: layout has {len(_stale_found)} stale window name(s) that no longer exist: {', '.join(_stale_found)}\n") sys.stderr.write(f"[GUI] Run the 'Reset Layout' command from the Command Palette (or delete {ini_path}) to regenerate.\n") def _trigger_hot_reload(self) -> bool: from src.hot_reloader import HotReloader result = HotReloader.reload_all(self) self._hot_reload_error = HotReloader.last_error return result def run(self) -> None: """Initializes the ImGui runner (HelloImGui) and starts the main application loop. Loads system themes, default styling metrics, fonts, and sets up window docking layouts. SSDL: `[I:hello_imgui] -> o-> [I:main_loop]` """ if "--headless" in sys.argv: print("Headless mode active") self._fetch_models(self.current_provider) import uvicorn headless_cfg = self.config.get("headless", {}) port = headless_cfg.get("port", 8000) api = self.create_api() uvicorn.run(api, host="0.0.0.0", port=port) else: from src.startup_profiler import startup_profiler if hasattr(self, "controller") and hasattr(self.controller, "mark_gui_run_started"): self.controller.mark_gui_run_started() with startup_profiler.phase("theme_load_from_config"): theme.load_from_config(self.config) with startup_profiler.phase("imgui_bundle_import"): from imgui_bundle import hello_imgui as _hi with startup_profiler.phase("RunnerParams_init"): self.runner_params = _hi.RunnerParams() self.runner_params.app_window_params.window_title = "manual slop" # (removed stale _t-based print; the phase() above already logs RunnerParams_init) if sys.platform == "win32": self.runner_params.app_window_params.borderless = True self.runner_params.app_window_params.borderless_closable = False self.runner_params.app_window_params.borderless_movable = False self.runner_params.app_window_params.borderless_resizable = True self.runner_params.app_window_params.window_geometry.size = (1680, 1200) self.runner_params.imgui_window_params.enable_viewports = getattr(self, "ui_multi_viewport", False) self.runner_params.imgui_window_params.remember_theme = True self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme() self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space # Enforce DPI Awareness and User Scale user_scale = theme.get_current_scale() self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale # Detect Monitor Refresh Rate for capping (Win32 only). # Uses the native EnumDisplaySettings call (~0.3ms) instead of spawning a # PowerShell/WMI subprocess (~350ms) so the first frame is not blocked. fps_cap = 60.0 if sys.platform == "win32": rate = _detect_refresh_rate_win32() if rate: fps_cap = rate # Enable idling with monitor refresh rate to effectively cap FPS self.runner_params.fps_idling.enable_idling = True self.runner_params.fps_idling.fps_idle = fps_cap self.runner_params.imgui_window_params.show_menu_bar = True self.runner_params.imgui_window_params.show_menu_view_themes = True self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder self.runner_params.ini_filename = "manualslop_layout.ini" def _profiled_setup_style() -> None: with startup_profiler.phase("setup_imgui_style"): theme.apply_current() def _profiled_post_init() -> None: with startup_profiler.phase("post_init"): self._post_init() self.runner_params.callbacks.show_gui = self._gui_func self.runner_params.callbacks.show_menus = self._show_menus self.runner_params.callbacks.load_additional_fonts = self._load_fonts self.runner_params.callbacks.setup_imgui_style = _profiled_setup_style self.runner_params.callbacks.post_init = _profiled_post_init self._fetch_models(self.current_provider) md_options = markdown_helper.get_renderer().options run_result = _run_immapp_result(self) if not run_result.ok: err = run_result.errors[0] if hasattr(self, "controller") and self.controller is not None: self.controller._gui_degraded_reason = err.message self.controller._last_imgui_assert = str(err.original) if err.original else err.message if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("run.immapp", err)) return # On exit (only reached on clean shutdown) self.shutdown() session_logger.close_session() def _load_fonts(self) -> None: from src.startup_profiler import startup_profiler with startup_profiler.phase("load_fonts"): # Set hello_imgui assets folder to the actual absolute path assets_dir = Path(__file__).parent.parent / "assets" if assets_dir.exists(): hello_imgui.set_assets_folder(str(assets_dir.absolute())) # Improved font rendering with oversampling config = imgui.ImFontConfig() config.oversample_h = 3 config.oversample_v = 3 font_path, font_size = theme.get_font_loading_params() if font_path: font_path = _resolve_font_path(font_path, assets_dir) result = _load_fonts_main_result(self, font_path, font_size, config) if not result.ok: if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("_load_fonts.main_font", result.errors[0])) else: self.main_font = None result = _load_fonts_mono_result(self, font_size, config) if not result.ok: if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("_load_fonts.mono_font", result.errors[0])) def _handle_approve_mma_step(self, user_data=None) -> None: """UI-level wrapper for approving a pending MMA step.""" self._handle_mma_respond(approved=True) def _handle_approve_spawn(self, user_data=None) -> None: """UI-level wrapper for approving a pending MMA sub-agent spawn.""" self._handle_mma_respond(approved=True) #TODO(Ed): Remove Exception based errors. def __getattr__(self, name: str) -> Any: if name == 'controller': raise AttributeError(name) if hasattr(self, 'controller') and hasattr(self.controller, name): return getattr(self.controller, name) raise AttributeError(name) def __setattr__(self, name: str, value: Any) -> None: if name != 'controller' and hasattr(self, 'controller') and hasattr(self.controller, name): setattr(self.controller, name, value) else: object.__setattr__(self, name, value) def _handle_generate_send(self) -> None: if not self.ui_selected_context_files and not getattr(self, "_pending_proceed_generate", False): self._pending_generation_action = 'generate' self.show_empty_context_modal = True else: self._pending_proceed_generate = False self.controller._handle_generate_send() def _handle_md_only(self) -> None: if not self.ui_selected_context_files and not getattr(self, "_pending_proceed_md_only", False): self._pending_generation_action = 'md_only' self.show_empty_context_modal = True else: self._pending_proceed_md_only = False self.controller._handle_md_only() @property def current_provider(self) -> str: return self.controller.current_provider @current_provider.setter def current_provider(self, value: str) -> None: self.controller.current_provider = value @property def current_model(self) -> str: return self.controller.current_model @current_model.setter def current_model(self, value: str) -> None: self.controller.current_model = value #TODO(Ed): Remove Exception based errors. def _get_active_capabilities(self) -> "VendorCapabilities": from src.vendor_capabilities import VendorCapabilities, get_capabilities #TODO(Ed): Remove Exception based errors. try: caps = get_capabilities(self.current_provider, self.current_model) except KeyError: caps = VendorCapabilities(vendor=self.current_provider, model=self.current_model, notes="unregistered") return _apply_runtime_caps_override(self, caps) @property def perf_profiling_enabled(self) -> bool: return self.controller.perf_profiling_enabled @perf_profiling_enabled.setter def perf_profiling_enabled(self, value: bool) -> None: self.controller.perf_profiling_enabled = value @property def missing_files(self) -> list[str]: return [f.path for f in self.context_files if not os.path.exists(f.path if os.path.isabs(f.path) else os.path.join(self.controller.active_project_root, f.path))] @property def app_debug_info(self) -> dict: return { "context_files": [f.path for f in self.context_files], "missing_files": self.missing_files, "screenshots": self.screenshots, "ui_new_context_preset_name": self.ui_new_context_preset_name, "target_context_preset_name": self.target_context_preset_name, "show_missing_files_modal": self.show_missing_files_modal, "active_project_root": str(self.controller.active_project_root), "project_keys": list(self.controller.project.keys()), "presets": list(self.controller.project.get('context_presets', {}).keys()) } def _take_snapshot(self) -> history.UISnapshot: """ Captures the current state of UI input parameters, system prompts, active discussions, and files list, returning a UISnapshot for history management. SSDL: `[Q:ui_state] -> [I:copy] -> [T:snapshot]` """ from src import history import copy return history.UISnapshot( ai_input = self.ui_ai_input, project_system_prompt = self.ui_project_system_prompt, global_system_prompt = self.ui_global_system_prompt, base_system_prompt = self.ui_base_system_prompt, use_default_base_prompt = self.ui_use_default_base_prompt, temperature = self.temperature, top_p = self.top_p, max_tokens = self.max_tokens, auto_add_history = self.ui_auto_add_history, disc_entries = copy.deepcopy(self.disc_entries), files = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.files], context_files = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_files], screenshots = list(self.screenshots) ) def _apply_snapshot(self, snapshot: history.UISnapshot) -> None: """Applies a previously captured UISnapshot back to the active UI state. Restores input fields, parameters, discussions, screenshots, and context files. SSDL Shape: `[I:lock_flag] -> [S:ui_state] -> [I:unlock]` """ self._is_applying_snapshot = True try: self.ui_ai_input = snapshot.ai_input self.ui_project_system_prompt = snapshot.project_system_prompt self.ui_global_system_prompt = snapshot.global_system_prompt self.ui_base_system_prompt = snapshot.base_system_prompt self.ui_use_default_base_prompt = snapshot.use_default_base_prompt self.temperature = snapshot.temperature self.top_p = snapshot.top_p self.max_tokens = snapshot.max_tokens self.ui_auto_add_history = snapshot.auto_add_history self.disc_entries = snapshot.disc_entries # Restore files as FileItem objects from src import models self.files = [] for f in snapshot.files: if isinstance(f, dict): self.files.append(models.FileItem.from_dict(f)) else: self.files.append(models.FileItem(path=str(f))) self.context_files = [] for f in snapshot.context_files: if isinstance(f, dict): self.context_files.append(models.FileItem.from_dict(f)) else: self.context_files.append(models.FileItem(path=str(f))) self.screenshots = list(snapshot.screenshots) self._last_ui_snapshot = snapshot # Update last snapshot to avoid immediate re-push finally: self._is_applying_snapshot = False # ?? TODO(Ed): Whats the point of this?? def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile: """Serializes the current window visibility states, popped-out panel layouts, and ImGui INI configurations into a WorkspaceProfile object. SSDL Shape: `[Q:ui_states] -> [B:ini_ready] -> [T:profile]` """ if not getattr(self, "_ini_capture_ready", False): self._ini_capture_ready = True ini = "" else: ini_result = _capture_workspace_profile_ini_result(self) if ini_result.ok: ini = ini_result.data else: if not hasattr(self, "_last_request_errors"): self._last_request_errors = [] self._last_request_errors.append(("_capture_workspace_profile", ini_result.errors[0])) ini = "" panel_states = { "ui_separate_context_preview": getattr(self, "ui_separate_context_preview", False), "ui_separate_message_panel": getattr(self, "ui_separate_message_panel", False), "ui_separate_response_panel": getattr(self, "ui_separate_response_panel", False), "ui_separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False), "ui_separate_task_dag": getattr(self, "ui_separate_task_dag", False), "ui_separate_usage_analytics": getattr(self, "ui_separate_usage_analytics", False), "ui_separate_tier1": getattr(self, "ui_separate_tier1", False), "ui_separate_tier2": getattr(self, "ui_separate_tier2", False), "ui_separate_tier3": getattr(self, "ui_separate_tier3", False), "ui_separate_tier4": getattr(self, "ui_separate_tier4", False), "ui_separate_external_tools": getattr(self, "ui_separate_external_tools", False), "ui_discussion_split_h": getattr(self, "ui_discussion_split_h", 300.0), } return models.WorkspaceProfile( name = name, ini_content = ini, show_windows = copy.deepcopy(self.show_windows), panel_states = panel_states ) def _apply_workspace_profile(self, profile: models.WorkspaceProfile): """Restores the window docking layout and popped-out panel visibility states from a saved WorkspaceProfile. SSDL Shape: `[I:load_ini] -> [S:ui_states]` """ imgui.load_ini_settings_from_memory(profile.ini_content) self.show_windows.update(profile.show_windows) for k, v in profile.panel_states.items(): if hasattr(self, k): setattr(self, k, v) def _handle_undo(self) -> None: """Reverts the application UI state to the previous snapshot in the history stack. DAG Render Context: Called by: _gui_func() (via hotkey Ctrl+Z) or undo button click. Calls: _take_snapshot(), _apply_snapshot(), HistoryManager.undo() """ sys.stderr.write(f"[DEBUG History] _handle_undo called. can_undo={self.history.can_undo}\n"); sys.stderr.flush() if not self.history.can_undo: return current = self._take_snapshot() entry = self.history.undo(current, "Undo Action") if entry: sys.stderr.write(f"[DEBUG History] Undoing to: {entry.description}\n"); sys.stderr.flush() self._apply_snapshot(entry.state) def _handle_jump_to_history(self, index: int) -> None: sys.stderr.write(f"[DEBUG History] Jumping to index {index}\n"); sys.stderr.flush() current = self._take_snapshot() entry = self.history.jump_to_undo(index, current, "Before Jump") if entry: self._apply_snapshot(entry.state) def _handle_redo(self) -> None: """Re-applies the next snapshot in the history stack (forward navigation). SSDL Shape: `[I:snapshot] -> [B:history] => [I:state]` """ sys.stderr.write(f"[DEBUG History] _handle_redo called. can_redo={self.history.can_redo}\n"); sys.stderr.flush() if not self.history.can_redo: return current = self._take_snapshot() entry = self.history.redo(current, "Redo Action") if entry: sys.stderr.write(f"[DEBUG History] Redoing to: {entry.description}\n"); sys.stderr.flush() self._apply_snapshot(entry.state) def shutdown(self) -> None: """Cleanly shuts down the app's background tasks, saves workspace layout configurations, forces a save of dirty registries/caches, and terminates the active thread pools. SSDL Shape: `[I:save_ini] -> [I:controller_shutdown]` """ ini_result = _shutdown_save_ini_result(self) if not ini_result.ok: if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("shutdown.save_ini", ini_result.errors[0])) self.controller.shutdown() def load_context_preset(self, name: str) -> None: preset = self.controller.load_context_preset(name) from src import models import copy self.context_files = [] for f in preset.files: fi = models.FileItem(path=f.path, view_mode=f.view_mode) fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} fi.ast_signatures = getattr(f, 'ast_signatures', False) fi.ast_definitions = getattr(f, 'ast_definitions', False) self.context_files.append(fi) self.screenshots = list(preset.screenshots) self.ui_file_paths = [f.path for f in preset.files] self.ui_screenshot_paths = list(preset.screenshots) self._update_context_file_stats() def delete_context_preset(self, name: str) -> None: self.controller.delete_context_preset(name) if getattr(self, "ui_active_context_preset", "") == name: self.ui_active_context_preset = "" @property def ui_file_paths(self) -> list[str]: return [f.path if hasattr(f, 'path') else str(f) for f in self.files] @ui_file_paths.setter def ui_file_paths(self, paths: list[str]) -> None: sys.stderr.write(f"[DEBUG] Setting ui_file_paths to: {paths}\n") sys.stderr.flush() old_files = {f.path: f for f in self.files if hasattr(f, 'path')} new_files = [] now = time.time() for p in paths: if p in old_files: new_files.append(old_files[p]) else: from src import models new_files.append(models.FileItem(path=p, injected_at=now)) self.files = new_files @property def ui_screenshot_paths(self) -> list[str]: return self.screenshots @ui_screenshot_paths.setter def ui_screenshot_paths(self, paths: list[str]) -> None: self.screenshots = paths def _toggle_command_palette(self) -> None: """Toggle the Command Palette visibility. Used as a custom_callback for tests since the keyboard shortcut (Ctrl+Shift+P) cannot be simulated via the hook API.""" self.show_command_palette = not self.show_command_palette if self.show_command_palette: if hasattr(self, '_command_palette_query'): self._command_palette_query = "" if hasattr(self, '_command_palette_selected'): self._command_palette_selected = 0 if hasattr(self, '_command_palette_input_focused'): self._command_palette_input_focused = False def _test_callback_func_write_to_file(self, data: str) -> None: """A dummy function that a custom_callback would execute for testing.""" # Ensure the directory exists if running from a different cwd os.makedirs("tests/artifacts", exist_ok=True) with open("tests/artifacts/temp_callback_output.txt", "w") as f: f.write(data) def _gui_func(self) -> None: """Main immediate-mode render loop callback executed on every frame. Dispatches keyboard shortcuts, renders the background shader, custom title bar, main dockspace, and handles popups/modals. SSDL Shape: `o-> [I:hotkeys] -> [I:title_bar] -> [I:main_interface] -> [I:modals]` ASCII Layout Map: +---------------------------------------------------------+ | [Menu: manual slop] [Windows] [Project] [Layout] [x] | +--------------------+-------------------+----------------+ | Project / Files | Discussion Hub | Operations | | | | | | [x] file1.py (Sig) | [User] | [Spawn Agent] | | [x] file2.py (Def) | I want you to... | | | | | | | +----------------+ | [AI] | [Exec clutch] | | | Presets | | Analyzing code... | | +--------------------+-------------------+----------------+ """ # One-shot: log when immapp first hands control to our render callback. The # span init -> here is window/GL/context creation + the font/style/post_init # callbacks (all opaque C++); the span here -> mark_first_frame_rendered is # the cost of the first full-UI render. Splitting them attributes the gap. if not getattr(self, "_gui_func_entered", False): self._gui_func_entered = True log_result = _gui_func_entry_log_result(self) if not log_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(('_gui_func.entry_log', log_result.errors[0])) # One-shot: kick off the controller's heavy-module warmup on the shared # io_pool once the FIRST frame has actually been painted. Waiting one frame # keeps the ~2s of SDK C-extension imports from holding the GIL during # window creation and font-atlas building, so the window appears at full # speed; the SDKs are then warmed by the time the user sends their first # message. start_warmup() is idempotent. if not getattr(self, "_preload_started", False): if getattr(self, "_first_frame_painted", False): self.controller.start_warmup() self._preload_started = True else: self._first_frame_painted = True io = imgui.get_io() if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r): self._trigger_hot_reload() if (io.key_ctrl and io.key_shift and not io.key_alt and not io.key_super and imgui.is_key_pressed(imgui.Key.p)): self.show_command_palette = not self.show_command_palette if self.show_command_palette: if hasattr(self, '_command_palette_query'): self._command_palette_query = "" if hasattr(self, '_command_palette_selected'): self._command_palette_selected = 0 render_custom_title_bar(self) render_shader_live_editor(self) render_history_window(self) pushed_prior_tint = False # Render background shader bg = bg_shader.get_bg() ws = imgui.get_io().display_size if bg.enabled: bg.render(ws.x, ws.y) theme.render_post_fx(ws.x, ws.y, self.ai_status, self.ui_crt_filter) if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func") result = _render_main_interface_result(self) if not result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_gui_func", result.errors[0])) self._handle_history_logic() if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func") return def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None: """Helper to render a window only if its toggle is active.""" if not flag_condition or not self.show_windows.get(name, False): return with imscope.window(name, self.show_windows[name]) as (exp, opened): self.show_windows[name] = bool(opened) if exp: render_func() def _render_controller_error_modal(self) -> None: """Thin delegation wrapper for render_controller_error_modal (drain plane). [SDM: src/gui_2.py:App._render_controller_error_modal]""" render_controller_error_modal(self) def _render_worker_error_indicator(self) -> None: """Thin delegation wrapper for _render_worker_error_indicator (drain plane). [SDM: src/gui_2.py:App._render_worker_error_indicator]""" _render_worker_error_indicator(self) def _render_last_request_errors_modal(self) -> None: """Thin delegation wrapper for _render_last_request_errors_modal (drain plane). [SDM: src/gui_2.py:App._render_last_request_errors_modal]""" _render_last_request_errors_modal(self) def _show_menus(self) -> None: global win32gui, win32con if win32gui is None: import win32con import win32gui win32con = win32con win32gui = win32gui with imscope.menu("manual slop") as (active): if active and imgui.menu_item("Quit", "Ctrl+Q", False)[0]: self.runner_params.app_shall_exit = True with imscope.menu("Windows") as (active): if (active): for w in self.show_windows.keys(): _, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w]) with imscope.menu("Project") as (active): if active: if imgui.menu_item("Save All", "", False)[0]: self._flush_to_project() self._flush_to_config() self.save_config() self.ai_status = "config saved" if imgui.menu_item("Reset Session", "", False)[0]: ai_client.reset_session() ai_client.clear_comms_log() self._tool_log.clear() self._comms_log.clear() self.ai_status = "session reset" self.ai_response = "" if imgui.menu_item("Generate MD Only", "", False)[0]: result = _show_menus_do_generate_result(self) if not result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_show_menus.do_generate", result.errors[0])) with imscope.menu("Layout") as (active): if active: if imgui.menu_item("Save Current...", "", False)[0]: self._show_save_workspace_profile_modal = True self._new_workspace_profile_name = "" imgui.separator() for profile_id, profile in self.workspace_profiles.items(): if imgui.menu_item(profile.name, "", False)[0]: self.controller._cb_load_workspace_profile(profile_id) imgui.separator() with imscope.menu("Delete Profile") as (active): if active: for profile_id, profile in self.workspace_profiles.items(): if imgui.menu_item(profile.name, "", False)[0]: self.controller._cb_delete_workspace_profile(profile_id, self._new_workspace_profile_scope) # Draw right-aligned window controls directly in the menu bar (Win32 only) if sys.platform == "win32": result = _show_menus_hwnd_result(self) if not result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_show_menus.hwnd", result.errors[0])) hwnd = result.data if hwnd: btn_w = 40 # Use window width (points) instead of display_size (pixels) for correct scaling window_w = imgui.get_window_width() bar_h = imgui.get_window_height() right_x = window_w - (btn_w * 3) # Drag area check using an explicit invisible button spanning the empty space curr_x = imgui.get_cursor_pos_x() drag_w = right_x - curr_x if drag_w > 0: imgui.invisible_button("##drag_area", (drag_w, bar_h)) if imgui.is_item_active() and imgui.is_mouse_dragging(0): # CRITICAL: We must reset ImGui's mouse_down state BEFORE passing control to Windows. # Otherwise, the Windows modal drag loop swallows the WM_LBUTTONUP event, # and ImGui thinks the mouse is permanently held down, causing "sticky" dragging. imgui.get_io().mouse_down[0] = False win32gui.ReleaseCapture() win32gui.SendMessage(hwnd, win32con.WM_NCLBUTTONDOWN, win32con.HTCAPTION, 0) imgui.push_style_color(imgui.Col_.button, imgui.ImVec4(0, 0, 0, 0)) result = _show_menus_is_max_result(self, hwnd) if not result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_show_menus.is_max", result.errors[0])) is_max = result.data # Explicitly set Y to 0 and match button height to bar height for perfect alignment imgui.set_cursor_pos((right_x, 0)) if imgui.button("_", (btn_w, bar_h)): win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) imgui.set_cursor_pos((right_x + btn_w, 0)) if imgui.button("[=]" if is_max else "[]", (btn_w, bar_h)): win32gui.ShowWindow(hwnd, win32con.SW_RESTORE if is_max else win32con.SW_MAXIMIZE) imgui.set_cursor_pos((right_x + btn_w * 2, 0)) imgui.push_style_color(imgui.Col_.button_hovered, theme.get_color("status_error")) if imgui.button("X", (btn_w, bar_h)): win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) imgui.pop_style_color() imgui.pop_style_color() def _handle_history_logic(self) -> None: """Logic for capturing UI state for undo/redo.""" if self._is_applying_snapshot: return result = _handle_history_logic_result(self) if not result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_handle_history_logic", result.errors[0])) def cb_load_prior_log(self, path: Optional[str] = None) -> None: if path is None: root = hide_tk_root() path = filedialog.askdirectory(title='Select Session Directory', initialdir=str(paths.get_logs_dir())) root.destroy() if path: self.controller.cb_load_prior_log(path) def _set_external_editor_default(self, editor_name: str) -> None: from src import models if "tools" not in self.config: self.config["tools"] = {} if "default_editor" not in self.config["tools"]: self.config["tools"]["default_editor"] = {} self.config["tools"]["default_editor"]["default_editor"] = editor_name self.save_config() self.ai_status = f"Default editor set to: {editor_name}" def _save_paths(self) -> None: self.config["paths"] = { "logs_dir": self.ui_logs_dir, "scripts_dir": self.ui_scripts_dir } cfg_path = paths.get_config_path() if cfg_path.exists(): shutil.copy(cfg_path, str(cfg_path) + ".bak") self.save_config() paths.initialize_paths(cfg_path) self.init_state() self.ai_status = 'paths applied and session reset' def _populate_auto_slices(self, f_item: models.FileItem) -> None: import re from pathlib import Path import os proj_dir = str(Path(self.controller.active_project_path).parent.resolve()) if getattr(self, 'controller', None) and self.controller.active_project_path else None abs_path = f_item.path if os.path.isabs(f_item.path) else os.path.join(proj_dir or '.', f_item.path) mcp_client.configure([{"path": abs_path}], [proj_dir] if proj_dir else None) outline_result = _populate_auto_slices_outline_result(self, f_item, abs_path) if not outline_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_populate_auto_slices.outline", outline_result.errors[0])) return outline = outline_result.data if not outline: return if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"): return pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE) file_read_result = _populate_auto_slices_file_read_result(self, f_item) if not file_read_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_populate_auto_slices.file_read", file_read_result.errors[0])) return text = file_read_result.data #TODO(Ed): Exception(Review) try: from src.fuzzy_anchor import FuzzyAnchor except ImportError: FuzzyAnchor = None for match in pattern.finditer(outline): kind, name, s_str, e_str = match.groups() s_line = int(s_str) e_line = int(e_str) if any(s.get('start_line') == s_line and s.get('end_line') == e_line for s in f_item.custom_slices): continue if FuzzyAnchor: slice_data = FuzzyAnchor.create_slice(text, s_line, e_line) else: slice_data = {"start_line": s_line, "end_line": e_line} slice_data['tag'] = 'auto-ast' slice_data['comment'] = name f_item.custom_slices.append(slice_data) def _update_context_file_stats(self) -> tuple[int, int]: if not hasattr(self, '_file_stats_cache'): self._file_stats_cache = {} if not hasattr(self, '_file_stats_queue'): self._file_stats_queue = [] if not hasattr(self, '_file_stats_worker_active'): self._file_stats_worker_active = False total_lines = 0 total_ast = 0 missing_keys = [] for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 cache_key = f"{f_path}_{mtime}" if cache_key not in self._file_stats_cache: missing_keys.append((f_path, cache_key)) else: cached = self._file_stats_cache[cache_key] stats = cached.data if hasattr(cached, "data") else cached total_lines += stats.get("lines", 0) total_ast += stats.get("ast_elements", 0) if missing_keys and not self._file_stats_worker_active: def _stats_worker() -> None: self._file_stats_worker_active = True try: for path, key in missing_keys[:10]: self._file_stats_cache[key] = aggregate.compute_file_stats(path) finally: self._file_stats_worker_active = False self.submit_io(_stats_worker) return total_lines, total_ast def _close_vscode_diff(self) -> None: if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: term_result = _close_vscode_diff_terminate_result(self) if not term_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(('_close_vscode_diff.terminate', term_result.errors[0])) self._vscode_diff_process = None def _apply_pending_patch(self) -> None: if not self._pending_patch_text: self._patch_error_message = "No patch to apply" return patch_result = _apply_pending_patch_result(self) if not patch_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_apply_pending_patch", patch_result.errors[0])) def _open_patch_in_external_editor(self) -> None: self._external_editor_clicked = True ext_result = _open_patch_in_external_editor_result(self) if not ext_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("_open_patch_in_external_editor", ext_result.errors[0])) def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: if src_idx == dst_idx: return new_tickets = list(self.active_tickets) ticket = new_tickets.pop(src_idx) new_tickets.insert(dst_idx, ticket) # Validate dependencies: a ticket cannot be placed before any of its dependencies id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)} valid = True for i, t in enumerate(new_tickets): deps = t.get('depends_on', []) for d_id in deps: if d_id in id_to_idx and id_to_idx[d_id] >= i: valid = False break if not valid: break if valid: self.active_tickets = new_tickets self._push_mma_state_update() def request_patch_from_tier4(self, error: str, file_context: str) -> None: tier4_result = request_patch_from_tier4_result(self, error, file_context) if not tier4_result.ok: if not hasattr(self, '_last_request_errors'): self._last_request_errors = [] self._last_request_errors.append(("request_patch_from_tier4", tier4_result.errors[0])) def bulk_execute(self) -> None: for tid in self.ui_selected_tickets: t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) if t: t['status'] = 'in_progress' self._push_mma_state_update() def bulk_skip(self) -> None: for tid in self.ui_selected_tickets: t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) if t: t['status'] = 'completed' self._push_mma_state_update() def bulk_block(self) -> None: for tid in self.ui_selected_tickets: t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) if t: t['status'] = 'blocked' self._push_mma_state_update() def _cb_kill_ticket(self, ticket_id: str) -> None: if self.controller and hasattr(self.controller, 'engine') and self.controller.engine: self.controller.engine.kill_worker(ticket_id) def _cb_block_ticket(self, ticket_id: str) -> None: t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) if t: t['status'] = 'blocked' t['manual_block'] = True t['blocked_reason'] = '[MANUAL] User blocked' changed = True while changed: changed = False for t in self.active_tickets: if t.get('status') == 'todo': for dep_id in t.get('depends_on', []): dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None) if dep and dep.get('status') == 'blocked': t['status'] = 'blocked' changed = True break self._push_mma_state_update() def _cb_unblock_ticket(self, ticket_id: str) -> None: t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) if t and t.get('manual_block', False): t['status'] = 'todo' t['manual_block'] = False t['blocked_reason'] = None changed = True while changed: changed = False for t in self.active_tickets: if t.get('status') == 'blocked' and not t.get('manual_block', False): can_run = True for dep_id in t.get('depends_on', []): dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None) if dep and dep.get('status') != 'completed': can_run = False break if can_run: t['status'] = 'todo' changed = True self._push_mma_state_update() def _post_init_callback_result(app: "App") -> Result[None]: """Drain-aware variant of App._post_init (L612 INTERNAL_SILENT_SWALLOW). Extracts the controller.on_warmup_complete callback registration from App._post_init into a Result-returning helper. On exception, returns Result(data=None, errors=[ErrorInfo]) so the legacy wrapper can drain the error to self._startup_timeline_errors. On success, the lambda callback is registered and Result(data=None) is returned. [C: src/gui_2.py:App._post_init (L612 legacy wrapper)] """ #Note(Ed): Exception(Thirdparty) try: app.controller.on_warmup_complete(lambda status: _on_warmup_complete_callback(app, status)) return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"on_warmup_complete callback registration failed: {e}", source="gui_2._post_init_callback_result", original=e, )]) def _run_immapp_result(app: "App") -> Result[None]: """Drain-aware variant of App.run immapp.run() call (L728 INTERNAL_SILENT_SWALLOW). Extracts the thirdparty immapp.run() invocation from App.run into a Result-returning helper. On exception (RuntimeError from IM_ASSERT or other native-bundle errors), converts to ErrorInfo. The legacy run method sets controller._gui_degraded_reason and _last_imgui_assert (the canonical degradation drain), appends to _startup_timeline_errors, and returns. NO logging: logging is NOT a drain per the user's principle 2026-06-17. [C: src/gui_2.py:App.run (L728 legacy wrapper)] """ #Note(Ed): Exception(Thirdparty) try: immapp.run(app.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=markdown_helper.get_renderer().options)) return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"immapp.run raised {type(e).__name__}: {e}", source="gui_2._run_immapp_result", original=e, )]) def _shutdown_save_ini_result(app: "App") -> Result[None]: """Drain-aware variant of App.shutdown save_ini try block (L1052 INTERNAL_SILENT_SWALLOW). Extracts the thirdparty imgui.save_ini_settings_to_disk try/except from App.shutdown into a Result-returning helper. On exception, converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The legacy shutdown method drains to self._startup_timeline_errors. [C: src/gui_2.py:App.shutdown (L1052 legacy wrapper)] """ #Note(Ed): Exception(Thirdparty) try: if hasattr(app, 'runner_params') and app.runner_params.ini_filename: imgui.save_ini_settings_to_disk(app.runner_params.ini_filename) return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"imgui.save_ini_settings_to_disk failed: {e}", source="gui_2._shutdown_save_ini_result", original=e, )]) def _gui_func_entry_log_result(app: "App") -> Result[None]: """Drain-aware variant of App._gui_func startup-timing log (L1152 INTERNAL_SILENT_SWALLOW). Extracts the first-frame startup-timing sys.stderr.write/flush from App._gui_func into a Result-returning helper. On exception (broken pipe, OSError on the stderr stream), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The legacy _gui_func method drains to self._last_request_errors. [C: src/gui_2.py:App._gui_func (L1152 legacy wrapper)] """ try: init_ts = getattr(app.controller, "_init_start_ts", None) if init_ts is not None: sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n") sys.stderr.flush() return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"gui_func entry log failed: {e}", source="gui_2._gui_func_entry_log_result", original=e, )]) def _close_vscode_diff_terminate_result(app: "App") -> Result[None]: """Drain-aware variant of App._close_vscode_diff terminate try block (L1466 INTERNAL_SILENT_SWALLOW). Extracts the self._vscode_diff_process.terminate() try/except from App._close_vscode_diff into a Result-returning helper. On exception (process already exited, invalid handle), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The legacy wrapper drains to self._last_request_errors and proceeds to set self._vscode_diff_process = None (preserving the original behavior). [C: src/gui_2.py:App._close_vscode_diff (L1466 legacy wrapper)] """ try: app._vscode_diff_process.terminate() return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"_vscode_diff_process.terminate failed: {e}", source="gui_2._close_vscode_diff_terminate_result", original=e, )]) def _focus_response_window_result() -> Result[None]: """Drain-aware variant of render_main_interface imgui.set_window_focus try block (L1647 INTERNAL_SILENT_SWALLOW). Extracts the thirdparty imgui.set_window_focus("Response") try/except from render_main_interface into a Result-returning helper. On exception (native bundle error, IM_ASSERT), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The caller drains to app._last_request_errors. [C: src/gui_2.py:render_main_interface (L1647 legacy wrapper)] """ try: imgui.set_window_focus("Response") # type: ignore[call-arg] return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"imgui.set_window_focus('Response') failed: {e}", source="gui_2._focus_response_window_result", original=e, )]) def _autosave_flush_result(app: "App") -> Result[None]: """Drain-aware variant of render_main_interface auto-save try block (L1693 INTERNAL_SILENT_SWALLOW). Extracts the auto-save flush_to_project + flush_to_config + save_config try/except from render_main_interface into a Result-returning helper. On exception (disk full, JSON parse error), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The caller drains to app._last_request_errors and continues with the GUI loop, preserving the original "don't disrupt the GUI loop" intent via the data plane rather than silent swallow. [C: src/gui_2.py:render_main_interface (L1693 legacy wrapper)] """ try: app._flush_to_project() app._flush_to_config() app.save_config() return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"autosave flush failed: {e}", source="gui_2._autosave_flush_result", original=e, )]) def _on_warmup_complete_callback_result(app: "App", status: dict) -> Result[None]: """Drain-aware variant of _on_warmup_complete_callback (L4911 INTERNAL_SILENT_SWALLOW). Extracts the warmup-completion body from _on_warmup_complete_callback into a Result-returning helper. On exception (status dict corruption, lock acquisition failure), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The legacy callback drains to app.controller._worker_errors with the controller lock acquired on append (thread-safety critical per sub-track 4 spec). [C: src/gui_2.py:_on_warmup_complete_callback (L4911 legacy wrapper)] """ try: app._warmup_completion_ts = time.time() pending = status.get("pending", []) completed = status.get("completed", []) failed = status.get("failed", []) total = len(pending) + len(completed) + len(failed) if failed: msg = f"Warmup finished with {len(failed)} failures ({total} modules)" else: msg = f"All imports ready ({total} modules)" if not hasattr(app, "_warmup_toast_lock"): import threading as _threading app._warmup_toast_lock = _threading.Lock() with app._warmup_toast_lock: if not hasattr(app, "_warmup_toast_messages"): app._warmup_toast_messages = [] app._warmup_toast_messages.append((time.time(), msg)) return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"on_warmup_complete_callback body failed: {e}", source="gui_2._on_warmup_complete_callback_result", original=e, )]) def _tier_stream_scroll_sync_result(app: "App", stream_key: str, content: str, imgui_mod) -> Result[None]: """Drain-aware variant of render_tier_stream_panel scroll-sync try block (L6908 INTERNAL_SILENT_SWALLOW). Extracts the narrow-except TypeError/AttributeError from render_tier_stream_panel into a Result-returning helper. The narrow except is the audit-classified SILENT_SWALLOW pattern: narrowing + logging NOT a drain per the user's principle 2026-06-17. On exception, converts to ErrorInfo. The caller (render_tier_stream_panel) drains to app._last_request_errors. [C: src/gui_2.py:render_tier_stream_panel (L6908 caller)] """ try: if len(content) != app._tier_stream_last_len.get(stream_key, -1): imgui_mod.set_scroll_here_y(1.0) app._tier_stream_last_len[stream_key] = len(content) return Result(data=None) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"tier stream scroll sync failed: {e}", source="gui_2._tier_stream_scroll_sync_result", original=e, )]) def _dag_cycle_check_result(app: "App") -> Result[bool]: """Drain-aware variant of render_task_dag_panel DAG cycle check (L7271 INTERNAL_SILENT_SWALLOW). Extracts the TrackDAG construction + has_cycle check try/except from render_task_dag_panel into a Result-returning helper. On a valid DAG, returns Result(data=False). On a cyclic DAG, returns Result(data=True). On exception (bad ticket dict, dag engine failure), converts to ErrorInfo (logging NOT a drain per the user's principle 2026-06-17). The caller drains to app._last_request_errors. [C: src/gui_2.py:render_task_dag_panel (L7271 caller)] """ from src.dag_engine import TrackDAG try: ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in app.active_tickets] temp_dag = TrackDAG(ticket_dicts) has_cycle = temp_dag.has_cycle() return Result(data=has_cycle) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"DAG cycle check failed: {e}", source="gui_2._dag_cycle_check_result", original=e, )]) def _ticket_id_max_int_result(tid: str) -> Result[int]: """Drain-aware variant of render_task_dag_panel ticket-ID parsing (L7315 INTERNAL_SILENT_SWALLOW). Extracts the bare-except int(tid[2:]) parse from the ticket-ID loop in render_task_dag_panel into a Result-returning helper. On success (valid T-XXX id), returns Result(data=int). On failure (T-abc, T-, etc.), returns Result(data=0, errors=[ErrorInfo]). The legacy loop skips malformed tickets via the data plane (logging NOT a drain per the user's principle 2026-06-17). The caller drains to app._last_request_errors. [C: src/gui_2.py:render_task_dag_panel (L7315 caller)] """ try: return Result(data=int(tid[2:])) except Exception as e: return Result(data=0, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"ticket id parse failed for {tid!r}: {e}", source="gui_2._ticket_id_max_int_result", original=e, )]) def main() -> None: app = App() app.run() if __name__ == "__main__": main() def render_main_interface(app: App) -> None: """Top-level per-frame orchestrator. Dispatches every subsystem in the correct order: error/stale overlay tints, perf bookends, GUI task draining, modal rendering, auto-save, comms/tool-log caching, and all dockable windows. SSDL: `[I:overlays] -> [I:task_drain] -> [I:modals] -> [I:windows] -> [I:popups]` ASCII Layout Map: Full-screen dockspace (managed by imgui_bundle): +-------------------+-------------------------------------+ | Project Settings | Discussion Hub | | AI Settings | +--------------------------------+ | | Files & Media | | History entries (scrollable) | | | Usage Analytics | +--------------------------------+ | | MMA Dashboard | [splitter] | | Task DAG | [Message] [Response] | | Tier 1..4 streams | | +-------------------+------------------+------------------+ | Operations Hub (tab-bar) | Theme window | | [Comms][Tools][Usage][Ext][Layouts] | | +---------------------------------------+-----------------+ (Modals: Approve Script, MMA Step/Spawn, Context, Patch...) """ if hasattr(app, "controller") and hasattr(app.controller, "mark_first_frame_rendered"): app.controller.mark_first_frame_rendered() render_error_tint(app) render_project_stale_tint(app) render_warmup_status_indicator(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 if app._pending_focus_response: app._pending_focus_response = False focus_result = _focus_response_window_result() if not focus_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(('render_main_interface.focus_response', focus_result.errors[0])) #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) from src.command_palette import render_palette_modal from src.commands import registry as _cmd_registry render_palette_modal(app, _cmd_registry.all()) render_preset_manager_window(app) render_tool_preset_manager_window(app) render_persona_editor_window(app) #TODO(Ed): Exception(Review) # Auto-save (every 60s) now = time.time() if now - app._last_autosave >= app._autosave_interval: app._last_autosave = now autosave_result = _autosave_flush_result(app) if not autosave_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(('render_main_interface.autosave', autosave_result.errors[0])) # 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", lambda: render_project_settings_hub(app)) app._render_window_if_open("Files & Media", lambda: render_files_and_media(app)) app._render_window_if_open("AI Settings", lambda: render_ai_settings_hub(app)) app._render_window_if_open("Usage Analytics", lambda: render_usage_analytics_panel(app), app.ui_separate_usage_analytics) app._render_window_if_open("MMA Dashboard", lambda: render_mma_dashboard(app)) app._render_window_if_open("Task DAG", lambda: render_task_dag_panel(app), 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", lambda: render_discussion_hub(app)) app._render_window_if_open("Operations Hub", lambda: render_operations_hub(app)) app._render_window_if_open("Message", lambda: render_message_panel(app), app.ui_separate_message_panel) app._render_window_if_open("Response", lambda: render_response_panel(app), app.ui_separate_response_panel) app._render_window_if_open("Tool Calls", lambda: render_tool_calls_panel(app), app.ui_separate_tool_calls_panel) app._render_window_if_open("External Tools", lambda: render_external_tools_panel(app), app.ui_separate_external_tools) app._render_window_if_open("Log Management", lambda: render_log_management(app)) app._render_window_if_open("Diagnostics", lambda: render_diagnostics_panel(app)) app._render_window_if_open("Context Preview", lambda: render_context_preview_window(app)) render_text_viewer_window(app) app.perf_monitor.end_frame() # Modals / Popups render_approve_script_modal(app) render_mma_modals(app) render_context_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 #region: Diagnostics & Analytics def render_usage_analytics_panel(app: App) -> None: """Renders the aggregate dashboard panel for usage, budgeting, cache analytics, tool performance metrics, session insights, and RAG status. SSDL:`[I:token_budget] -> [I:cache_panel] -> [I:tool_analytics] -> [I:session_insights] -> [I:rag_status]` ASCII Layout Map: +---------------------------------------------------------+ | [Prompt Utilization sub-panel] | | --- | | [Cache Analytics sub-panel] | | --- | | [Tool Usage sub-panel] | | --- | | [Session Insights sub-panel] | | --- | | [RAG: indexing...] (clickable status badge) | +---------------------------------------------------------+ """ 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 = theme.get_color("status_success") elif status == "error": color = theme.get_color("status_error") else: color = theme.get_color("text_disabled") 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_diagnostics_panel(app: App) -> None: """Renders the Diagnostics window. Shows FPS, frame time, CPU%, and input lag in a summary table; when profiling is on, shows per-component moving-average timings, optional sparkline graphs, and the diagnostic message log. SSDL: `[I:perf_summary_table] -> [B:enable_profiling] => [I:component_timings_table] -> [I:graphs] -> [I:diag_log]` ASCII Layout Map: +---------------------------------------------------------+ | Performance Telemetry [ ] Enable Profiling | | +----------------+---------+-------+ | | | FPS | 120.0 | [ ] | | | | Frame Time(ms) | 8.33 | [ ] | | | | CPU % | 4.2 | [ ] | | | +----------------+---------+-------+ | | (when profiling enabled) | | Detailed Component Timings | | +--------------------+------+-----+------+------+----+ | | | Component | Avg | Cnt | Max | Min | G | | | +--------------------+------+-----+------+------+----+ | | Diagnostic Log | | +--------------------+------+---------------------+ | | | 2026-06-12 22:00 | info | Cache rebuilt | | | +--------------------+------+---------------------+ | +---------------------------------------------------------+ """ 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(theme.get_color("status_error"), 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_cache_panel(app: App) -> None: """Renders Gemini cache analytics. Shows cache age, TTL remaining as a colour-coded progress bar (green > 50%, yellow > 20%, red otherwise), and a [Clear Cache] button. Skips rendering for non-Gemini providers. SSDL: `[I:cache_stats] -> [B:progress_bar] => [B:clear_cache]` ASCII Layout Map: +---------------------------------------------------------+ | Cache Analytics | | Age: 4m 12s TTL: 55m 48s (93%) | | [============================================= ] 93% | | [Clear Cache] | | Cache cleared - will rebuild on next request | +---------------------------------------------------------+ """ 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 = theme.get_color("status_success") if ttl_pct < 20: color = theme.get_color("status_error") elif ttl_pct < 50: color = theme.get_color("status_warning") 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(theme.get_color("status_success"), "Cache cleared - will rebuild on next request") if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") def render_tool_analytics_panel(app: App) -> None: """Renders a breakdown of tool execution telemetry: calls, average latency ms, and failure rate percentage per tool. Results are sorted by invocation count descending. SSDL: `[I:tool_stats] -> [B:stats_table]` ASCII Layout Map: +---------------------------------------------------------+ | Tool Usage | | +------------------------+-------+----------+-------+ | | | Tool | Count | Avg (ms) | Fail% | | | +------------------------+-------+----------+-------+ | | | py_get_definition | 42 | 35 | 0% | | | | run_powershell | 18 | 820 | 5% | | | +------------------------+-------+----------+-------+ | +---------------------------------------------------------+ """ 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(theme.get_color("status_error"), 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: """Renders prompt token utilization breakdown: session totals, cache read/creation stats, a colour-coded utilisation progress bar, component breakdown table (System/Tools/History), per-tier MMA cost table, trim warnings, and cache activity badge. SSDL: `[I:session_tokens] -> [B:utilization_bar] -> [B:breakdown_table] -> [B:tier_costs_table]` ASCII Layout Map: +---------------------------------------------------------+ | Prompt Utilization | | Tokens: 24,200 (In: 18,000 Out: 6,200) Latency: 2.1s | | Cache Read: 12,000 Creation: 2,000 | | [============================== ] 63.2% | | 24,200 / 38,000 tokens (13,800 remaining) | | +-------------+--------+------+ | | | System | 8,000 | 33% | | | | Tools | 4,000 | 17% | | | | History | 12,200 | 50% | | | +-------------+--------+------+ | | MMA Tier Costs | | | Tier 1 | gemini | 14,000 | $0.0012 | | | ⚠ WARNING: Next call will trim history | | Cache Usage: ACTIVE | Age: 252s / 3600s | +---------------------------------------------------------+ """ 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 = theme.get_color("status_success") elif pct < 80.0: color = theme.get_color("status_warning") else: color = theme.get_color("status_error") 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) if caps.local: cost_str = "Free (local)" elif caps.cost_tracking: cost_str = f"${cost:.4f}" else: cost_str = "-" imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", cost_str, width=-1, color=theme.get_color("status_success")) 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()) if caps.local: total_str = "Free (local)" elif caps.cost_tracking: total_str = f"${tier_total:.4f}" else: total_str = "-" render_selectable_label(app, "session_total_cost", f"Session Total: {total_str}", width=-1, color=theme.get_color("status_success")) else: imgui.text_disabled("No MMA tier usage data") if stats.get("would_trim"): imgui.text_colored(theme.get_color("status_warning"), "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() caps = app._get_active_capabilities() if not caps.caching: imgui.text_disabled(f"Cache Usage: N/A (not supported by {app.current_provider}/{app.current_model})") else: cache_stats = getattr(app.controller, '_cached_cache_stats', {}) if cache_stats.get("cache_exists"): age = cache_stats.get("cache_age_seconds", 0) ttl = cache_stats.get("ttl_seconds", 3600) imgui.text_colored(C_LBL(), f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s") else: imgui.text_disabled("Cache Usage: INACTIVE") if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") #endregion: Diagnostics & Analytics #region: Logging def render_log_management(app: App) -> None: """Renders the Log Management window. Enables browsing, starring (whitelisting), and loading of prior log sessions from the log registry. Provides one-click prune and refresh actions. SSDL: `[B:refresh | load | prune] -> [I:sessions_table] => [B:load | star/unstar per row]` ASCII Layout Map: +---------------------------------------------------------+ | Log Management [x] | | [Refresh Registry] [Load Log] [Force Prune Logs] | | +------------+------------------+-----+--------+------+ | | | Session ID | Start Time | Star| Size KB| Msgs | | | +------------+------------------+-----+--------+------+ | | | 20260612_2 | 2026-06-12 22:00 | YES | 120 | 84 | | | | | | | [Load] [Unstar] | | | 20260611_1 | 2026-06-11 18:30 | NO | 340 | 210 | | | | | | | [Load] [Star] | | +------------+------------------+-----+--------+------+ | +---------------------------------------------------------+ """ 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 app._log_registry is None: from src import log_registry app._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) app._log_registry.load_registry() if exp: if imgui.button("Refresh Registry"): if app._log_registry is not None: app._log_registry.load_registry() 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(theme.get_color("status_warning"), "YES") else: imgui.text("NO") imgui.table_next_column() metadata = s_data.get("metadata") or {} 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") #endregion: Logging #region: Project Management def render_project_settings_hub(app: App) -> None: """Renders the Project Settings Hub: a two-tab container for Projects and Paths configuration. SSDL: `[I:tab_bar] => [I:projects_panel] | [I:paths_panel]` ASCII Layout Map: +---------------------------------------------------------+ | [Projects] [Paths] | | +-----------------------------------------------------+ | | | | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ 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: """Renders the project configuration panel. Allows setting execution mode, repository path, output/conductor directories, managing projects list, and global layout toggles (word-wrap, auto-scroll). SSDL: `[I:active_project] -> [B:execution_mode] -> [B:directories] -> [I:project_files] -> [B:project_actions] -> [B:toggles]` ASCII Layout Map: +---------------------------------------------------------+ | Active: my-feature-project | | Execution Mode: [native v] | | Git Directory: [C:\projects\manual_slop___] [Browse] | | Output Dir: [C:\output____________________] [Browse] | | Conductor Dir: [C:\projects\...\conductor___] [Browse] | | Project Files | | +-----------------------------------------------------+ | | | [x] my-feature * (active) C:\proj\... | | | | [x] chore-cleanup C:\proj\... | | | +-----------------------------------------------------+ | | [Add Project] [New Project] [Save All] | | [ ] Word-Wrap [ ] Auto-scroll Comms [ ] Auto-scroll | +---------------------------------------------------------+ """ 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() app.save_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: """Renders the System Path Configuration panel. Shows source-tagged logs and scripts directory fields, each with a Browse button, and Apply / Reset buttons. SSDL: `[I:path_fields] => [B:apply_reset]` ASCII Layout Map: +---------------------------------------------------------+ | System Path Configuration | | Logs Directory (Source: env) | | [C:\projects\manual_slop\logs___________] [Browse] | | Scripts Directory (Source: default) | | [C:\projects\manual_slop\scripts________] [Browse] | | [Apply] [Reset] | +---------------------------------------------------------+ """ 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) render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") imgui.separator() if imgui.button("Apply", imgui.ImVec2(120, 0)): app._save_paths() imgui.same_line() if imgui.button("Refresh Paths", imgui.ImVec2(140, 0)): paths.initialize_paths(paths.get_config_path()) app.init_state() app.ai_status = "paths reloaded from config.toml" if imgui.is_item_hovered(): imgui.set_tooltip("Re-read [paths] section from config.toml and rebuild the PathsConfig singleton. Use after editing config.toml directly or after importing a new config.") imgui.same_line() if imgui.button("Reset", imgui.ImVec2(120, 0)): app.init_state() app.ai_status = "paths reset to defaults" if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_paths_panel") #endregion: Project Management #region: AI Settings def render_ai_settings_hub(app: App) -> None: """Groups and renders all AI-related configuration panels in a unified hub sidebar. Includes persona selection, LLM provider settings, system prompts, RAG config, and tools. SSDL: `[I:persona_selector] -> [B:provider_header] -> [B:system_prompts_header] -> [B:rag_header] -> [I:agent_tools]` ASCII Layout Map: +---------------------------------------------------------+ | [Persona Selector sub-panel] | | > Provider & Model (collapsible) | | [Provider sub-panel content] | | > System Prompts (collapsible) | | [System Prompts sub-panel content] | | > RAG Settings (collapsible) | | [RAG sub-panel content] | | [Agent Tools sub-panel] | +---------------------------------------------------------+ """ 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: """Renders RAG configuration panel. Exposes enable toggle, vector-store and embedding provider selectors, chunk size/overlap inputs, RAG status label, and a Rebuild Index button. SSDL: `[B:rag_switch] -> [B:combo_selectors] -> [B:chunking_inputs] => [B:rebuild_index]` ASCII Layout Map: +---------------------------------------------------------+ | [x] Enable RAG | | Vector Store Provider: [chroma v] | | Embedding Provider: [gemini v] | | Chunk Size: [512] | | Chunk Overlap: [64] | | Status: indexing... | | [Rebuild Index] | +---------------------------------------------------------+ """ 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'] #NOTE(Ed): Exception(Thirdparty) 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'] #NOTE(Ed): Exception(Thirdparty) 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: """Renders the System Prompts panel. Exposes global preset selector + multiline edit, base prompt toggle (default vs custom) with diff + reset, and project-level preset selector + multiline edit. SSDL: `[B:global_preset_combo] -> [I:global_text] -> [B:base_prompt_header] -> [I:base_text] -> [B:project_preset_combo] -> [I:project_text]` ASCII Layout Map: +---------------------------------------------------------+ | Global System Prompt (all projects) | | [coding-assistant v] [Manage Presets] | | +-----------------------------------------------------+ | | | You are a coding assistant... | | | +-----------------------------------------------------+ | | [x] Use Default Base System Prompt | | [Reset to Default] [Show Diff] (?) | | > Base System Prompt (foundational instructions) | | [read-only or editable text area] | | Project System Prompt | | [feature-mode v] [Manage Presets] | | +-----------------------------------------------------+ | | | Focus on the rendering pipeline refactor... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ 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: """Renders the Active Tool Presets & Biases collapsible section. Shows a preset combo, a Manage Presets button, and a Bias Profile combo. Displays a disabled notice when tool calling is unsupported by the active provider/model. SSDL: `[B:collapsing_header] => [B:preset_combo] -> [B:manage_presets] -> [B:bias_combo]` ASCII Layout Map: +---------------------------------------------------------+ | > Active Tool Presets & Biases (open by default) | | Tool Preset: [mcp-only v] [Manage Presets] | | Bias Profile: [None v] | +---------------------------------------------------------+ """ caps = app._get_active_capabilities() if not caps.tool_calling: if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): imgui.text_disabled(f"(tools not supported by {app.current_provider}/{app.current_model})") return 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 = "" #NOTE(Ed): Exception(Thirdparty) 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())) #NOTE(Ed): Exception(Thirdparty) 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(theme.get_color("status_error"), "[HIGH]"); imgui.same_line() elif tool.weight == 4: imgui.text_colored(theme.get_color("status_warning"), "[PREF]"); imgui.same_line() elif tool.weight == 2: imgui.text_colored(theme.get_color("status_warning"), "[REJECT]"); imgui.same_line() elif tool.weight <= 1: imgui.text_colored(theme.get_color("text_disabled"), "[LOW]"); imgui.same_line() imgui.text(tool.name); imgui.same_line(180) mode = tool.approval if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): tool.approval = "auto" imgui.same_line() if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): tool.approval = "ask" imgui.tree_pop() def render_provider_panel(app: App) -> None: r"""Renders the LLM provider configuration panel. Allows selection of API providers, active models, hyper-parameters (temperature, max tokens, Top-P), history limits, and Gemini CLI binaries. SSDL: `[I:providers] -> [B:provider_combo] -> [B:model_listbox] -> [B:parameters_sliders] => [B:cli_path_browse]` ASCII Layout Map: +---------------------------------------------------------+ | Provider | | [gemini v] [Local] | | [V2 Badges: JSON/STRUCT/SYS/TOOL/C_IN/C_OUT/C_TRM...] | | --- | | Model | | +-----------------------------------------------------+ | | | gemini-1.5-pro | | | | gemini-1.5-flash * | | | +-----------------------------------------------------+ | | --- | | Parameters | | [======= ] 0.70 Temp: |0.70| | | [=========== ] 4096 Top-P: [1.00] | | [====== ] 8192 MaxTok: [8192] | | History Truncation Limit: |900000| | | (If gemini_cli active): | | --- | | Gemini CLI | | Session ID: cba123 | | [Reset CLI Session] | | Binary Path: | | [C:\tools\gemini.exe_______________________] [Browse] | +---------------------------------------------------------+ """ 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 ai_client.PROVIDERS: if imgui.selectable(p, p == app.current_provider)[0]: app.current_provider = p imgui.end_combo() caps = app._get_active_capabilities() if caps.local: imgui.same_line() imgui.text_colored(theme.get_color("status_success"), " [Local]") if imgui.is_item_hovered(): base_url: str = "" if app.current_provider == "llama": base_url = getattr(ai_client, "_llama_base_url", "") imgui.set_tooltip(f"Local backend: {base_url or 'unknown'}" if base_url else "Local backend") _render_v2_capability_badges(caps) 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() # Max Tokens caps = app._get_active_capabilities() max_tokens_cap = max(1, caps.context_window) 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, max_tokens_cap) 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: """Renders the Persona Selection panel. Allows selecting profiles, editing persona definitions, overriding system parameters (models, presets, prompts, bias profiles) and loading preset contexts. SSDL: `[I:personas] -> [B:persona_combo] => [B:manage_personas]` ASCII Layout Map: +---------------------------------------------------------+ | Persona | | [assistant v] [Manage Personas] | +---------------------------------------------------------+ """ 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 #TODO(Ed): Exception(Review) try: app.load_context_preset(persona.context_preset) except KeyError as e: app.ai_status = f"persona context preset missing: {e}" app.ui_active_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_base_prompt_diff_modal(app: App) -> None: """Renders a popup modal showing a unified diff between the default system prompt and the current custom base prompt. SSDL: `[I:unified_diff] -> [I:diff_view_box] => [B:close]` """ 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(theme.get_color("diff_header"), line.rstrip()) elif line.startswith("+"): imgui.text_colored(theme.get_color("diff_added"), line.rstrip()) elif line.startswith("-"): imgui.text_colored(theme.get_color("diff_removed"), 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_save_preset_modal(app: App) -> None: """Renders the popup modal for saving the current ImGui window layout preset. SSDL: `[I:preset_inputs] => [B:save_cancel]` ASCII Layout Map: +---------------------------------------------------------+ | Save Layout Preset [X] | +---------------------------------------------------------+ | Preset Name: | | | | | | [Save] [Cancel] | +---------------------------------------------------------+ """ 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 app.save_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_preset_manager_content(app: App, is_embedded: bool = False) -> None: """Renders the core prompt preset manager interface including the sidebar preset selector and the preset prompt content text editor. SSDL: `[I:presets_list] -> [B:new_preset] -> [I:editor_meta] -> [I:editor_textbox] => [B:save_delete]` ASCII Layout Map: +---------------------------------------------------------+ | [New Preset] | | --- | | | preset-a | Editing Prompt Preset: preset-a | | | preset-b | --- | | | Name: |preset-a| | | | Scope: (o) Global ( ) Project | | | --- | | | Prompt Content: [Pop out MD Preview] | | +--------------------------------+ | | | | You are a coding assistant... | | | | +--------------------------------+ | | ------------------------------------------------------- | | [Save] [Delete] [Close] | +---------------------------------------------------------+ """ 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_windows["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: """Renders the window container for the Prompt Presets Manager. SSDL: `[I] -> [I:window] => [I:preset_manager_content]` ASCII Layout Map: +=================== Prompt Presets Manager =============+ | | | [ Renders render_preset_manager_content here ] | | | +========================================================+ """ 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: """Renders the tool presets and tool capability profiles editor layout. SSDL: `[I:tool_presets] -> [B:new_tool_preset] -> [I:capability_toggles] -> [I:tools_list] => [B:save_delete]` ASCII Layout Map: +---------------------------------------------------------+ | [New Preset] | | --- | | | preset-a | Editing Tool Preset: preset-a | | | preset-b | --- | | | Name: |preset-a| | | | Scope: (o) Global ( ) Project | | | --- | | | > Categories & Tools | | | Filter: [All v] | | | v file_io | | | +----------------------------+ | | | | read_file ( ) Off ( ) Auto (o) Ask | | | | write_file ( ) Off ( ) Auto (o) Ask | | | +----------------------------+ | | | > Bias Profiles | | | +------------------------------+ | | | | [New Profile] | Name: |bias| | | | | | profile-a | --- | | | | +------------------------------+ | | ------------------------------------------------------- | | [Save] [Delete] [Close] | +---------------------------------------------------------+ """ 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] with imscope.child("tp_scroll", 0, h1, 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] with imscope.table(f"tt_{cat_name}", 2, imgui.TableFlags_.borders_inner_v) as opened_table: if opened_table: 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"); curr_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"); curr_cat_tools.append(tool) else: tool.approval = "ask" imgui.tree_pop() 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)): bias_save_result = _render_tool_preset_bias_save_result(app) if not bias_save_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_tool_preset_bias_save", bias_save_result.errors[0])) 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: """Renders the window container for the Tool Preset Manager. SSDL: `[I] -> [I:window] => [I:tool_preset_manager_content]` ASCII Layout Map: +==================== Tool Preset Manager ===============+ | | | [ Renders render_tool_preset_manager_content here ] | | | +========================================================+ """ 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_tool_preset_manager_content(app, is_embedded=is_embedded) def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: """Renders the Persona Editor window, allowing creating, deleting, and modifying persona settings (prompts, preferred models list, tool presets, aggregation strategy, etc). SSDL: `[I:personas_list] -> [B:new_persona] -> [I:settings_editor] -> [I:models_list] -> [I:system_prompt_box] => [B:save_delete]` ASCII Layout Map: +========================== Persona Editor ========================+ | [New Persona] | | --- | | | assistant | Editing Persona: assistant | | | researcher | --- | | | Name: |assistant| | | | Scope: (o) Global ( ) Project | | | --- | | | > Preferred Models | | | [+] 1. gemini - flash (T:0.7, P:1.0, M:0) | | | [-] 2. anthropic - claude-3-opus | | | Provider: [anthropic v] | | | Model: [claude-3-opus v] | | | Temp: [0.7] Max Tokens: [4096] | | | [Add Model] | | | > System Prompt | | | +---------------------------------------+ | | | | You are a helpful assistant... | | | | +---------------------------------------+ | | ---------------------------------------------------------------- | | [Save] [Delete] [Close] | +==================================================================+ """ 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]: import copy; #TODO(Ed): Review local import 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 "" 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 = ai_client.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: with imscope.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.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(): result = _render_persona_editor_save_result(app) if not result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("render_persona_editor.save", result.errors[0])) 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() #endregion: AI Settings #region: Context Management def render_files_and_media(app: App) -> None: """Renders the inventory of files and screenshots. Allows adding files or directories to the inventory and attaching files or screenshots to the active context. SSDL: `[I:inventory] -> [B:add_files_folders] -> [I:screenshots] => [B:add_screenshots]` ASCII Layout Map: +---------------------------------------------------------+ | Files collapsing header | | +---------------------------------------------------+ | | | [Act] [Path] [Status] | | | | [+] src/gui_2.py Active | | | +---------------------------------------------------+ | | [Add Files to Inventory] [Add Directory] | | Screenshots collapsing header | | +---------------------------------------------------+ | | | [x] /path/to/screenshot1.png | | | +---------------------------------------------------+ | | [Add Screenshots] | +---------------------------------------------------------+ """ if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open): with imscope.group(): to_remove_idx = -1 app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower()) file_indices = {id(f): idx for idx, f in enumerate(app.files)} grouped = aggregate.group_files_by_dir(app.files) if imgui.begin_table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): imgui.table_setup_column("Act", imgui.TableColumnFlags_.width_fixed, 60) imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch) imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 70) imgui.table_headers_row() imgui.end_table() for dir_name, g_files in sorted(grouped.items()): with imscope.tree_node_ex(f"{dir_name}##files_dir", imgui.TreeNodeFlags_.default_open) as dir_open: if imgui.begin_table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): imgui.table_setup_column("Act", imgui.TableColumnFlags_.width_fixed, 60) imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch) imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 70) # imgui.table_headers_row() if dir_open: for f_item in g_files: i = file_indices[id(f_item)] imgui.table_next_row() imgui.table_set_column_index(0) fpath = f_item.path if hasattr(f_item, 'path') else str(f_item) display_name = os.path.basename(fpath) in_context = any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files) is_cached = any(fpath in c for c in getattr(app, '_cached_files', [])) if imgui.button(f"+##add_f_{i}"): if not in_context: from src import models new_item = models.FileItem(path=fpath) app.context_files.append(new_item) app._populate_auto_slices(new_item) imgui.same_line() if imgui.button(f"x##rem_f_{i}"): to_remove_idx = i imgui.table_set_column_index(1) imgui.text(display_name) if imgui.is_item_hovered(): imgui.set_tooltip(fpath) imgui.table_set_column_index(2) if in_context: imgui.text_colored(theme.get_color("status_success"), "Active") elif is_cached: imgui.text_colored(theme.get_color("status_info"), "Cached") else: imgui.text_disabled(" - ") imgui.end_table() if to_remove_idx != -1: app.files.pop(to_remove_idx) imgui.dummy(imgui.ImVec2(0, 5)) if imgui.button("Add Files to Inventory"): r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy() from src import models 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 Directory"): r = hide_tk_root(); dirpath = filedialog.askdirectory(); r.destroy() if dirpath: existing = {f.path if hasattr(f, "path") else str(f) for f in app.files} for root, _dirs, files in os.walk(dirpath): for fname in files: full = os.path.join(root, fname) if full not in existing: app.files.append(models.FileItem(path=full)) existing.add(full) imgui.separator() if imgui.collapsing_header("Screenshots", imgui.TreeNodeFlags_.default_open): with imscope.child("Shots_child", -1, 150, True): to_rem_shot = -1 for i, s in enumerate(app.screenshots): if imgui.button(f"x##s{i}"): to_rem_shot = i imgui.same_line(); imgui.text(s) if to_rem_shot != -1: app.screenshots.pop(to_rem_shot) caps = app._get_active_capabilities() imgui.begin_disabled(not caps.vision) 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) imgui.end_disabled() if not caps.vision: imgui.same_line() imgui.text_disabled(f"(vision not supported by {app.current_model}; attachments would be ignored)") return def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None: """Renders a batch actions control bar. Allows batch-changing view modes of selected files, selecting/deselecting all, adding files, and generating context preview markdown. SSDL: `[I:context_files] -> [B:mode_batch_buttons] -> [B:selection_buttons] -> [B:all_add_del] => [B:preview]` ASCII Layout Map: +---------------------------------------------------------+ | Batch: [Full] [Summary] [Skeleton] [Outline] [Masked] | | [Sel All] [Unsel All] [Add Files] [Add All] [Del] | | [Preview] | Total: X files, Y lines, Z AST elements | +---------------------------------------------------------+ """ 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##add_btn"): 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() if imgui.button("Preview##ctx"): if not app.context_files: app.context_preview_text = "# Context Composition Empty\n\nNo files have been added to the context composition yet." else: preview_result = _render_context_batch_actions_preview_result(app) app.context_preview_text = preview_result.data if not preview_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_context_batch_actions_preview", preview_result.errors[0])) app.show_windows["Context Preview"] = True imgui.same_line() imgui.text(f" | Total: {len(app.context_files)} files, {total_lines} lines, {total_ast} AST elements") def render_add_context_files_modal(app: App) -> None: """Renders a modal popup listing files in the project inventory that can be batch-added to the active context. SSDL: `[I:picker_list] -> [B:checkboxes] -> [B:add_selected_button] => [B:cancel_button]` ASCII Layout Map: +---------------------------------------------------------+ | Select Context Files [X] | +---------------------------------------------------------+ | Select files from project to add to context: | | +-----------------------------------------------------+ | | | [ ] src/gui_2.py | | | | [x] src/models.py | | | +-----------------------------------------------------+ | | [Add Selected] [Cancel] | +---------------------------------------------------------+ """ 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_context_composition_panel(app: App) -> None: """Renders the Context Composition panel containing loaded project files, presets, and visual screenshot files. Displays token stats, batch files actions, and collapsible trees. SSDL: `[I:stats] -> [I:batch_actions] -> [I:files_table] -> [I:presets] -> [I:screenshots]` ASCII Layout Map: +-------------------------------------------------------------+ | > Context Composition | | [Presets v] [Add Files] [Batch Actions] | | +-------------------------------------------------------+ | | | [-] src/ | | | | [x] gui_2.py (Sig) [Inspect] [Slices] [Remove] | | | | [ ] models.py [Inspect] [Slices] [Remove] | | | +-------------------------------------------------------+ | | > Screenshots | +-------------------------------------------------------------+ """ 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() render_context_presets(app) imgui.separator() if imgui.collapsing_header("Screenshots"): render_context_screenshots(app) def render_ast_inspector_modal(app: App) -> None: """Renders the 'Structural File Editor' modal dialog. Provides an interactive tree-sitter AST inspector to mask classes, methods, or functions and extract custom annotated slices (Definition, Signature, Hide). SSDL: `[B:open_modal?] -> [Q:outline] -> [I:render_ast_tree]` ASCII Layout Map: +-------------------------------------------------------------+ | Structural File Editor: src/gui_2.py [X] | +-------------------------------------------------------------+ | [Filter: ] [Clear Filters] [Preset: Sig-Only v] | +--------------------+----------------------------------------+ | AST Tree | File Preview | | | | | [-] Class: App | 302: class App: | | [x] __init__ | 305: def __init__(self): | | [ ] run | | +--------------------+----------------------------------------+ | [Add selected slices] [Close] | +-------------------------------------------------------------+ """ if getattr(app, 'show_structural_editor_modal', False): imgui.open_popup('Structural File Editor') app.show_structural_editor_modal = False imgui.set_next_window_size(imgui.ImVec2(1400, 900), imgui.Cond_.first_use_ever) expanded, opened = imgui.begin_popup_modal('Structural File Editor', True, imgui.WindowFlags_.none) if opened: if expanded: if app.ui_editing_slices_file is None: imgui.close_current_popup() else: f_item = app.ui_editing_slices_file f_path = f_item.path if hasattr(f_item, "path") else str(f_item) if f_path != getattr(app, '_cached_ast_file_path', None): outline_result = _render_ast_inspector_outline_result(app, f_path) if not outline_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("render_ast_inspector.outline", outline_result.errors[0])) outline = outline_result.data app._cached_ast_nodes = [] import re #TODO(Ed): Review local import 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) }) content_result = _render_ast_inspector_file_content_result(app, f_path) if not content_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("render_ast_inspector.file_content", content_result.errors[0])) app._cached_ast_file_lines = ["Error loading file content."] app.text_viewer_content = "Error loading file content." else: app._cached_ast_file_lines = content_result.data.splitlines() app.text_viewer_content = content_result.data app._cached_ast_file_path = f_path imgui.text(f"Editing Structure: {f_path}") imgui.separator() avail = imgui.get_content_region_avail() table_height = max(100.0, avail.y - imgui.get_frame_height_with_spacing() * 2 - 20) if imgui.begin_table('structure_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v, imgui.ImVec2(0, table_height)): imgui.table_setup_column("AST & Slices", imgui.TableColumnFlags_.width_fixed, 400) imgui.table_setup_column("Content Preview", imgui.TableColumnFlags_.width_stretch) imgui.table_next_row() imgui.table_next_column() # --- LEFT COLUMN: AST Tree & Slice Management --- imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 0), True) # if True: if imgui.collapsing_header("AST Tree", imgui.TreeNodeFlags_.default_open): if not getattr(app, '_cached_ast_nodes', None): imgui.text("No AST nodes found.") else: with imscope.table('ast tree members', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.sizing_stretch_prop): imgui.table_setup_column("symbol", imgui.TableColumnFlags_.width_fixed, 400) imgui.table_setup_column("option", imgui.TableColumnFlags_.width_stretch) imgui.table_next_row() imgui.table_next_column() for node in app._cached_ast_nodes: imgui.table_next_row() imgui.table_next_column() 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}") if imgui.is_item_hovered(): app._hovered_ast_node = full_path imgui.table_next_column() # btn_width = 150 # avail_width = imgui.get_content_region_avail().x # do_align = avail_width > btn_width if isinstance(avail_width, (int, float)) else False # if do_align: imgui.same_line((avail_width - btn_width)) # else: imgui.same_line() # imgui.same_line((avail_width - btn_width) * 0.925) if not hasattr(f_item, 'ast_mask'): f_item.ast_mask = {} 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.separator() if imgui.collapsing_header("Custom Slices", imgui.TreeNodeFlags_.default_open): if not hasattr(f_item, 'custom_slices'): f_item.custom_slices = [] imgui.text_colored(C_IN(), "Highlight lines in right pane to add slices.") if imgui.button("Add Selection as Slice"): if getattr(app, '_slice_sel_start', -1) != -1 and getattr(app, '_slice_sel_end', -1) != -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'] = "" f_item.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 imgui.same_line() if imgui.button("Auto-Populate"): app._populate_auto_slices(f_item) to_remove = -1 tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"]) for idx, slc in enumerate(f_item.custom_slices): imgui.push_id(f"slc_row_{idx}"); imgui.text(f"#{idx+1}: L{slc['start_line']}-{slc['end_line']}"); imgui.same_line() current_tag = slc.get('tag', '') if current_tag not in tags and current_tag: tags.append(current_tag) tag_idx = tags.index(current_tag) if current_tag in tags else 0 imgui.set_next_item_width(100) ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags) if ch_tag: slc['tag'] = tags[new_tag_idx] imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', '')) if changed_comm: slc['comment'] = new_comm imgui.same_line() if imgui.button("X"): to_remove = idx imgui.pop_id() if to_remove != -1: f_item.custom_slices.pop(to_remove) imgui.end_child() imgui.table_next_column() # --- RIGHT COLUMN: Content Preview with Highlights --- with imscope.child("ast_content_scroll", imgui.ImVec2(0, 0), True): if not getattr(app, '_cached_ast_file_lines', None): 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 pos = imgui.get_cursor_screen_pos() line_height = imgui.get_text_line_height() avail_width = imgui.get_content_region_avail().x # 1. AST Highlight 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 = getattr(f_item, 'ast_mask', {}).get(deepest_node['full_path'], 'hide') if mode == 'def': draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("slice_auto", alpha=0.15))) elif mode == 'sig': draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("slice_selection", alpha=0.15))) elif deepest_node and deepest_node['full_path'] == getattr(app, '_hovered_ast_node', None): draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("status_warning", alpha=0.2))) # 2. Slice Highlight if hasattr(f_item, 'custom_slices'): is_auto = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in f_item.custom_slices if slc.get('tag') == 'auto-ast') is_man = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in f_item.custom_slices if slc.get('tag') != 'auto-ast') if is_man: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("slice_manual", alpha=0.2))) elif is_auto and mode == 'hide': draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("slice_auto", alpha=0.1))) # 3. Active Selection Highlight if getattr(app, '_slice_sel_start', -1) != -1 and getattr(app, '_slice_sel_end', -1) != -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 + avail_width, pos.y + line_height), imgui.get_color_u32(theme.get_color("slice_selection", alpha=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(imgui.HoveredFlags_.allow_when_blocked_by_active_item) and imgui.is_mouse_down(0): app._slice_sel_end = line_num imgui.end_table() imgui.separator() if imgui.button("Close", imgui.ImVec2(120, 0)): app.ui_editing_slices_file = None app.ui_inspecting_ast_file = None imgui.close_current_popup() imgui.end_popup() if not opened: app.ui_editing_slices_file = None app.ui_inspecting_ast_file = None def render_save_workspace_profile_modal(app: App) -> None: """Renders a popup modal for saving the current workspace profile (projects, layout, configuration). SSDL: `[I:name_input] -> [B:scope_radio] -> [B:save_button] => [B:cancel_button]` ASCII Layout Map: +---------------------------------------------------------+ | Save Workspace Profile [X] | +---------------------------------------------------------+ | Name: [ ] | | Scope: (o) Project ( ) Global | | [Save] [Cancel] | +---------------------------------------------------------+ """ 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: """Renders the Context Presets configuration panel. Enables naming, saving, loading, and deleting context presets. SSDL: `[I:presets_list] -> [B:save_new_preset] => [B:presets_action_buttons]` ASCII Layout Map: +---------------------------------------------------------+ | Context Presets | | Preset Name: [ ] [Save Current] | | ------------------------------------------------------- | | my-preset (2 files, 0 shots) [Load] [Delete]| +---------------------------------------------------------+ """ 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: """Renders a list of the currently attached screenshot image files. SSDL: `[I:screenshots]` ASCII Layout Map: +---------------------------------------------------------+ | /path/to/screenshot_1.png | | /path/to/screenshot_2.png | +---------------------------------------------------------+ """ for i, s in enumerate(app.screenshots): imgui.text(s) def render_context_files_table(app: App) -> None: """Renders a two-column table mapping active context files to their AST view modes (Definition, Signature, Hide) and slice indicators. Grouped by directories. SSDL: `[I:group_by_dir] -> o-> [I:dir_node] -> o-> [I:file_row]` ASCII Layout Map: +-------------------------------------------------------------+ | File | Flags | +--------------------------------------+----------------------+ | [-] src/ | | | [x] gui_2.py | [Sig v] [Inspect] [X] | | [x] models.py | [Def v] [Inspect] [X] | +--------------------------------------+----------------------+ """ 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 f_item.auto_aggregate = is_sel changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) if changed_sel: f_item.auto_aggregate = is_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() _abs_p = f_path if os.path.isabs(f_path) else os.path.join(app.controller.active_project_root, f_path) _exists = os.path.exists(_abs_p) mtime = os.path.getmtime(_abs_p) if _exists else 0 cache_key = f"{f_path}_{mtime}" stats_raw = app._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0}) stats = stats_raw.data if hasattr(stats_raw, "data") else stats_raw f_name = os.path.basename(f_path) imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})") if not _exists: imgui.same_line() imgui.text_colored(imgui.ImVec4(1.0, 0.0, 0.0, 1.0), "[MISSING]") if f_path.lower().endswith(('.py', '.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): imgui.same_line() if imgui.button(f"[Structure]##{i}"): app.ui_editing_slices_file = f_item app.ui_inspecting_ast_file = f_item app.show_structural_editor_modal = 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(theme.get_color("status_warning"), "[Slices Active]") def render_context_presets(app: App) -> None: """Renders context preset management controls (combobox selector, update button, save-as, and delete active). SSDL: `[I:presets] -> [B:presets_combo] -> [B:save_as_button] => [B:delete_active]` ASCII Layout Map: +---------------------------------------------------------+ | [Presets Combo v] | [Update] | | [New Preset Input ] | [Save As] | | | [Delete Active] | +---------------------------------------------------------+ """ 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 with imscope.table("ctx_presets_layout", 2, imgui.TableFlags_.none): imgui.table_next_column() imgui.set_next_item_width(-1) 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.table_next_column() if active: if imgui.button("Update##override", imgui.ImVec2(-1, 0)): preset_files = [] for f in app.context_files: import copy from src import models p = f.path if hasattr(f, 'path') else str(f) vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn)) preset = models.ContextPreset(name=active, files=preset_files, screenshots=list(app.screenshots)) app.controller.save_context_preset(preset) else: imgui.text_disabled("No active preset") imgui.table_next_row() imgui.table_next_column() imgui.set_next_item_width(-1) changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", "") or "") if changed: app.ui_new_context_preset_name = new_name imgui.table_next_column() if imgui.button("Save As##ctx", imgui.ImVec2(-1, 0)) or getattr(app, "_pending_save_ctx_click", False): app._pending_save_ctx_click = False name = getattr(app, "ui_new_context_preset_name", "").strip() if name: missing = [] root = app.controller.active_project_root for f in app.context_files: path = f.path if hasattr(f, "path") else str(f) if not os.path.isabs(path): full_path = os.path.join(root, path) else: full_path = path if not os.path.exists(full_path): missing.append(path) if missing: app.missing_context_files = missing app.show_missing_files_modal = True app.target_context_preset_name = name else: preset_files = [] for f in app.context_files: import copy from src import models p = f.path if hasattr(f, 'path') else str(f) vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn)) preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots)) app.controller.save_context_preset(preset) app.ui_new_context_preset_name = "" if active: imgui.table_next_row() imgui.table_next_column() imgui.table_next_column() if imgui.button("Delete Active", imgui.ImVec2(-1, 0)): app.delete_context_preset(active) app.ui_active_context_preset = "" def render_snapshot_tab(app: App) -> None: """Renders the Snapshot tab-bar, containing the resolved aggregate markdown and active system prompts. SSDL: `[I:aggregate_text] -> [B:copy_buttons] => [I:preview_panes]` ASCII Layout Map: +---------------------------------------------------------+ | [Aggregate MD] [System Prompt] | | ------------------------------------------------------- | | [Copy] | | +-----------------------------------------------------+ | | | Resolved Markdown Content... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ with imscope.tab_bar("snapshot_tabs"): with imscope.tab_item("Aggregate MD") as (exp, _): if exp: 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) with imscope.child("last_agg_md", 0, 0, True): markdown_helper.render(display_md, context_id="snapshot_agg") with imscope.tab_item("System Prompt") as (exp, _): if exp: if imgui.button("Copy"): imgui.set_clipboard_text(app.last_resolved_system_prompt) with imscope.child("last_sys_prompt", 0, 0, True): markdown_helper.render(app.last_resolved_system_prompt, context_id="snapshot_sys") def render_empty_context_modal(app: App) -> None: """Renders a popup modal warning the user when the context composition is empty. SSDL: `[I:warning_text] -> [B:proceed_button] => [B:cancel_button]` ASCII Layout Map: +---------------------------------------------------------+ | Empty Context Warning [X] | +---------------------------------------------------------+ | WARNING: Empty Context Composition | | You are attempting to generate a response... | | [Proceed Anyway] [Cancel] | +---------------------------------------------------------+ """ if app.show_empty_context_modal: imgui.open_popup("Empty Context Warning") app.show_empty_context_modal = False if imgui.begin_popup_modal("Empty Context Warning", True, imgui.WindowFlags_.always_auto_resize)[0]: imgui.text_colored(theme.get_color("status_warning"), "WARNING: Empty Context Composition") imgui.text("You are attempting to generate a response without any files selected.") imgui.text("This may result in poor AI performance or loss of project context.") imgui.separator() if imgui.button("Proceed Anyway", imgui.ImVec2(150, 0)): if app._pending_generation_action == 'generate': app.controller._handle_generate_send() elif app._pending_generation_action == 'md_only': app.controller._handle_md_only() app._pending_generation_action = None imgui.close_current_popup() imgui.same_line() if imgui.button("Cancel", imgui.ImVec2(120, 0)): imgui.close_current_popup() imgui.end_popup() def render_context_modals(app: App) -> None: """Dispatches rendering calls to sub-modals relating to context management (empty context warning, file picker, missing files warning, and AST inspector). SSDL: `[I] -> [I:modals_dispatch]` ASCII Layout Map: Conditionally opens (in order): [Empty Context Warning modal] [Select Context Files modal] [Missing Files Warning modal] [Structural File Editor modal] """ render_empty_context_modal(app) render_add_context_files_modal(app) if app.show_missing_files_modal: imgui.open_popup("Missing Files Warning") app.show_missing_files_modal = False if imgui.begin_popup_modal("Missing Files Warning", True, imgui.WindowFlags_.always_auto_resize)[0]: imgui.text("The following files are missing from disk:") imgui.separator() imgui.begin_child("missing_files_list", imgui.ImVec2(0, 150), True) for f in app.missing_context_files: imgui.text_colored(theme.get_color("status_error"), f) imgui.end_child() imgui.separator() if imgui.button("Save Anyway") or getattr(app, "_pending_save_anyway_click", False): app._pending_save_anyway_click = False name = app.target_context_preset_name preset_files = [] for f in app.context_files: import copy from src import models p = f.path if hasattr(f, 'path') else str(f) vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn)) preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots)) app.controller.save_context_preset(preset) app.ui_new_context_preset_name = "" imgui.close_current_popup() imgui.same_line() if imgui.button("Cancel"): imgui.close_current_popup() imgui.end_popup() render_ast_inspector_modal(app) def _get_context_composition_state(app: App) -> tuple: files_state = [] for f in app.context_files: p = f.path if hasattr(f, 'path') else str(f) vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' agg = f.auto_aggregate if hasattr(f, 'auto_aggregate') else False slc = tuple((s.get('start_line'), s.get('end_line'), s.get('tag'), s.get('comment')) for s in getattr(f, 'custom_slices', [])) mask = tuple(sorted(getattr(f, 'ast_mask', {}).items())) files_state.append((p, vm, agg, slc, mask)) screenshots_state = tuple(app.screenshots) return (tuple(files_state), screenshots_state) def _check_auto_refresh_context_preview(app: App) -> None: current_state = _get_context_composition_state(app) if not hasattr(app, "_last_context_preview_state") or app._last_context_preview_state != current_state: app._last_context_preview_state = current_state if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots: app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation." return if getattr(app, "_is_generating_preview", False): app._pending_preview_refresh = True return app._is_generating_preview = True def worker(): try: result = _worker_context_preview_result(app) if not result.ok: if app.controller is not None and hasattr(app.controller, "_worker_errors"): with app.controller._worker_errors_lock: app.controller._worker_errors.append(("_check_auto_refresh_context_preview.worker", result.errors[0])) finally: app._is_generating_preview = False if getattr(app, "_pending_preview_refresh", False): app._pending_preview_refresh = False # This will trigger again on next GUI frame because _last_context_preview_state # will be slightly behind if another change happened during the thread. # Or we just clear the state so it re-triggers. app._last_context_preview_state = None app.controller.submit_io(worker) def render_context_preview_window(app: App) -> None: """Renders the Context Preview window. Automatically refreshes aggregated context previews in the background and renders formatted markdown text. SSDL: `[I:preview_text] -> [B:clipboard_button] => [I:preview_box]` ASCII Layout Map: +---------------------------------------------------------+ | Context Preview | +---------------------------------------------------------+ | [Close] [Copy to Clipboard] | | +-----------------------------------------------------+ | | | # Context Composition | | | | ## src/gui_2.py | | | | ...aggregated markdown... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ _check_auto_refresh_context_preview(app) with imscope.window("Context Preview", app.show_windows["Context Preview"]) as (exp, opened): app.show_windows["Context Preview"] = bool(opened) if not opened: app.ui_separate_context_preview = False if exp: if imgui.button("Close"): app.show_windows["Context Preview"] = False app.ui_separate_context_preview = False imgui.same_line() if imgui.button("Copy to Clipboard"): imgui.set_clipboard_text(app.context_preview_text) with imscope.child("ctx_preview_scroll", 0, 0, True): markdown_helper.render(app.context_preview_text, context_id="ctx_preview") #endregion: Context Management #region: Discussions def render_discussion_hub(app: App) -> None: """Top-level hub for the Discussion panel. Houses tabs for Discussion, Context Composition, Context Preview (inline or detached), Snapshot, and Takes. SSDL: `[B:pop_out_toggle] -> [I:tab_bar] => [I:active_tab_content]` ASCII Layout Map: +---------------------------------------------------------+ | [ ] Pop Out Context Preview | | [Discussion] [Context Composition] [Context Preview] | | [Snapshot] [Takes] | | +-----------------------------------------------------+ | | | | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ _check_auto_refresh_context_preview(app) ch, popped = imgui.checkbox("Pop Out Context Preview", getattr(app, "ui_separate_context_preview", False)) if ch: app.ui_separate_context_preview = popped app.show_windows["Context Preview"] = popped 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) if not getattr(app, "ui_separate_context_preview", False): with imscope.tab_item("Context Preview") as (exp, opened): if exp: if imgui.button("Copy to Clipboard"): imgui.set_clipboard_text(app.context_preview_text) with imscope.child("ctx_preview_scroll_tab", 0, 0, True): markdown_helper.render(app.context_preview_text, context_id="ctx_preview_tab") 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_thinking_trace(app: App, entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: """Renders a collapsible thinking-trace section within a discussion entry bubble. Displays each reasoning segment labeled by its marker tag (e.g. [thinking]). SSDL: `[I:segment_list] -> [B:read_pure_toggle] => [I:trace_child]` ASCII Layout Map: +----------------------------------------------------------+ | > Monologue (3 traces) | | [Pure] / [Read] Selectable toggle | | +----------------------------------------------------+ | | | [thinking] ...reasoning text... | | | | --- | | | | [thinking] ...continued... | | | +----------------------------------------------------+ | +----------------------------------------------------------+ """ if not segments: return # Tint thinking trace background slightly differently with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor", alpha=0.7)), \ theme.ai_text_style(): with imscope.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: thinking_read_mode = entry.get("thinking_read_mode", True) if imgui.button(f"[Pure]##think_pure_{entry_index}" if thinking_read_mode else f"[Read]##think_read_{entry_index}"): entry["thinking_read_mode"] = not thinking_read_mode imgui.same_line() imgui.text_colored(C_TC(), "Selectable toggle") 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(C_TC(), f"[{marker}]") if thinking_read_mode: if app.ui_word_wrap: with imscope.text_wrap(imgui.get_content_region_avail().x): imgui.text(content) else: imgui.text(content) else: render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1) imgui.separator() def render_discussion_entry(app: App, entry: dict, index: int) -> None: """Renders a single discussion message entry as a colored visual bubble. Supports collapsible states, read/edit mode toggles, role selection, input/output token metrics, and action footers. SSDL: `[I:header] -> [B:collapsed] => [I:preview] | [I:body] -> [I:footer]` ASCII Layout Map: +-------------------------------------------------------------+ | [-] Entry #3 [Role: AI v] [Read] @2026-06-12 in:12 | +-------------------------------------------------------------+ | | | [thinking trace: ] | | | | "I think the right approach is..." | | | +-------------------------------------------------------------+ | [Ins] [Del] [Branch] | +-------------------------------------------------------------+ """ with imscope.id(f"disc_{index}"): role = entry.get("role", "User") bg_col = theme.get_role_tint(role) draw_list = imgui.get_window_draw_list() p_min = imgui.get_cursor_screen_pos() full_width = imgui.get_content_region_avail().x # Start Background Layer (Channel 0: Background, Channel 1: Foreground) draw_list.channels_split(2) draw_list.channels_set_current(1) imgui.begin_group() # FORCE GROUP TO FULL WIDTH to prevent Markdown table squashing imgui.dummy(imgui.ImVec2(full_width, 0)) # Header controls 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"], id_suffix=f"disc_btn_{index}") 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", "") usage = entry.get("usage", {}) if ts_str or usage: imgui.same_line() if ts_str: imgui.text_colored(C_SUB(), str(ts_str)) if usage: inp, out, cache = usage.get("input_tokens", 0), usage.get("output_tokens", 0), usage.get("cache_read_input_tokens", 0) u_str = f" in:{inp} out:{out}" + (f" cache:{cache}" if cache else "") imgui.same_line(); imgui.text_colored(theme.get_color("status_info"), u_str) 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"): if entry in app.disc_entries: app.disc_entries.remove(entry) imgui.end_group() draw_list.channels_merge() 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 += "..." imgui.text_colored(C_SUB(), preview) else: # Body content - FORCE START ON NEW LINE to prevent horizontal squashing imgui.new_line() imgui.spacing() thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) if thinking_segments: render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content) imgui.spacing() if read_mode: render_discussion_entry_read_mode(app, entry, index) else: if not (bool(thinking_segments) and not has_content): # Ensure multiline editor uses full width imgui.set_next_item_width(-1) ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) imgui.end_group() # Finalize Background Tint draw_list.channels_set_current(0) p_max = imgui.get_item_rect_max() # Ensure full width coverage of the panel p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0) draw_list.channels_merge() imgui.separator() def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None: """Renders a discussion entry in read-only mode. Parses the markdown content, isolates retrieved context sections (RAG chunks), handles custom definition/AST links, and invokes the markdown syntax highlighter. SSDL: `[I:extract_rag] -> [B:definitions?] => [I:markdown]` ASCII Layout Map: +-------------------------------------------------------------+ | > Retrieved Context (collapsible) | | > Chunk 1: src/gui_2.py (collapsible) | | [Source] ...chunk text... | | --- | | > [Definition: foo from gui_2.py (line 42)] (collapsible) | | [Source] ```python...``` | | --- | | ## Markdown heading | | Rendered response text... | +-------------------------------------------------------------+ """ with imscope.id(f"read_{index}"): 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 = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'); app.show_windows["Text Viewer"] = 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 = list(pattern.finditer(content)) # Provide a stable width by using a group and ensuring it starts on a new line imgui.begin_group() with theme.ai_text_style(): if not matches: markdown_helper.render(content, context_id=f"disc_{index}") else: last_idx = 0 for m_idx, match in enumerate(matches): before = content[last_idx:match.start()] if before: 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 = path, res, (Path(path).suffix.lstrip(".") if Path(path).suffix else "text"); app.show_windows["Text Viewer"] = True if code_block: markdown_helper.render(code_block, context_id=f"disc_{index}_c_{m_idx}") last_idx = match.end() after = content[last_idx:] if after: markdown_helper.render(after, context_id=f"disc_{index}_a") imgui.end_group() def render_history_window(app: App) -> None: """Renders the Undo/Redo History window. Displays past UI snapshots in reverse chronological order and allows reverting to prior states. SSDL: `[I:history_list] -> [B:undo_redo_buttons] => [B:selectable_snapshots]` """ 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(history) def render_session_insights_panel(app: App) -> None: """Renders session productivity insights, displaying total tokens, API call counts, burn rates, total costs, completed ticket counts, and token efficiency. SSDL: `[I:insights] -> [I:telemetry_texts]` """ 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_prior_session_view(app: App) -> None: """Renders a historical read-only view of a loaded prior discussion session, complete with collapsing message bubbles and bubble colors. SSDL: `[I:prior_entries] -> [B:exit_button] -> [I:scroll_list]` """ with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor")): if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True imgui.same_line() imgui.text_colored(theme.get_color("status_warning"), f"({len(app.prior_disc_entries)} entries)") imgui.separator() avail = imgui.get_content_region_avail() with imscope.child("prior_scroll", imgui.ImVec2(avail.x, avail.y), imgui.WindowFlags_.horizontal_scrollbar): for idx, entry in enumerate(app.prior_disc_entries): 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(theme.get_color("text_disabled"), str(ts)) content = entry.get("content", "") if collapsed: imgui.same_line(); preview = content.replace("\n", " ")[:80] if len(content) > 80: preview += "..." imgui.text_colored(theme.get_color("text_disabled"), 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: """Renders a blinking red status indicator when the AI is currently executing, sending, streaming, or running shell commands. SSDL: `[I:ai_status] -> [I:blinking_text]` """ 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 = theme.get_color("status_error", alpha=alpha) imgui.text_colored(c, "THINKING..."); def _on_warmup_complete_callback(app: App, status: dict) -> None: """Thread-safe callback registered with controller.on_warmup_complete() during App._post_init. Records the completion timestamp; the indicator function uses it to show a brief "ready" tag. Also appends a message to a lock-protected list that the indicator (or the diagnostics panel) can read. Runs on a background _io_pool thread; only sets primitive state. """ cb_result = _on_warmup_complete_callback_result(app, status) if not cb_result.ok: controller = getattr(app, "controller", None) if controller is not None and hasattr(controller, "_worker_errors_lock"): with controller._worker_errors_lock: if not hasattr(controller, "_worker_errors"): controller._worker_errors = [] controller._worker_errors.append(('_on_warmup_complete_callback', cb_result.errors[0])) def render_warmup_status_indicator(app: App) -> None: """Renders a transient warmup status indicator in the main interface frame. Shows progress of AppController's background module imports. SSDL: `[I:warmup_status] -> [I:status_text]` """ controller = getattr(app, "controller", None) if controller is None: return if not hasattr(controller, "warmup_status"): return result = _render_warmup_status_indicator_result(app) if not result.ok: if not hasattr(controller, "_worker_errors"): controller._worker_errors = [] with controller._worker_errors_lock: controller._worker_errors.append(("render_warmup_status_indicator", result.errors[0])) return status = result.data pending = status.get("pending", []) completed = status.get("completed", []) failed = status.get("failed", []) if pending: total = len(pending) + len(completed) + len(failed) done = len(completed) + len(failed) c = theme.get_color("status_warning") imgui.text_colored(c, f"Warming up... ({done}/{total})") return if failed: c = theme.get_color("status_error") imgui.text_colored(c, f"Imports: {len(failed)} failed") return # Steady state + transient 3s "ready" tag after completion. ts = getattr(app, "_warmup_completion_ts", 0.0) if ts > 0 and (time.time() - ts) < 3.0: total = len(completed) + len(failed) c = theme.get_color("status_success") imgui.text_colored(c, f"All imports ready ({total} modules)") return # No render: warmup done, no failures, transient window expired. def render_synthesis_panel(app: App) -> None: """Renders a panel for synthesizing multiple discussion takes.""" imgui.text("Select takes to synthesize:") discussions = app.project.get('discussion', {}).get('discussions', {}) if not isinstance(getattr(app, 'ui_synthesis_selected_takes', None), dict): app.ui_synthesis_selected_takes = {name: False for name in discussions} if not isinstance(getattr(app, 'ui_synthesis_prompt', None), str): 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_comms_history_panel(app: App) -> None: """Renders the communications history log panel. Displays outgoing requests, incoming responses, and tool call inputs/outputs in chronological order. SSDL: `[I:ai_status] -> [B:clear_exit_buttons] -> [I:direction_colors] -> [I:entries_scroll_list]` """ if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_comms_history_panel") st_col = theme.get_color("text_disabled") if theme.is_nerv_active(): st_col = theme.get_color("status_success") # 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() avail = imgui.get_content_region_avail() with imscope.child("comms_scroll", imgui.ImVec2(avail.x, avail.y), imgui.WindowFlags_.horizontal_scrollbar): log_to_render = app._comms_log_cache for i, entry in enumerate(log_to_render): 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(theme.get_color("text_disabled"), 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(theme.get_color("status_error"), f"[{ticket_id}]") imgui.same_line() d_col_fn = DIR_COLORS.get(direction, C_VAL) k_col_fn = KIND_COLORS.get(kind, C_VAL) imgui.text_colored(d_col_fn(), direction); imgui.same_line() imgui.text_colored(k_col_fn(), 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, payload, [{"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 if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") def render_takes_panel(app: App) -> None: """Renders the Takes & Synthesis panel. Lists all discussion takes in a table, allows switching or deleting them, and provides a multi-take synthesis workflow. SSDL: `[I:takes_table] -> [B:switch/delete] -> [I:synthesis_config] => [B:generate_synthesis]` ASCII Layout Map: +---------------------------------------------------------+ | Takes & Synthesis | | +----------+----------+-----------------------------+ | | | Name | Entries | Actions | | | +----------+----------+-----------------------------+ | | | main | 14 | [Switch] [Delete] | | | | take_2 | 3 | [Switch] [Delete] | | | +----------+----------+-----------------------------+ | | Synthesis | | [ ] main [ ] take_2 | | Synthesis Prompt: | | +-----------------------------------------------------+ | | | Compare and reconcile these takes... | | | +-----------------------------------------------------+ | | [Generate Synthesis] | +---------------------------------------------------------+ """ imgui.text("Takes & Synthesis") imgui.separator() discussions = app.project.get('discussion', {}).get('discussions', {}) if not isinstance(getattr(app, 'ui_synthesis_selected_takes', None), dict): app.ui_synthesis_selected_takes = {name: False for name in discussions} if not isinstance(getattr(app, 'ui_synthesis_prompt', None), str): 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_discussion_entries(app: App) -> None: """Renders the scrollable list of all discussion entry bubbles. When a focus agent is active (ui_focus_agent), only entries matching that agent's persona (or User) are shown. SSDL: `[I:filter_by_agent?] -> [I:scroll_child] => [I:entry_bubbles]` ASCII Layout Map: +---------------------------------------------------------+ | +-----------------------------------------------------+ | | | Entry #1 [User] ... | | | | Entry #2 [AI] ... | | | | Entry #3 [User] ... | | | | <- scroll-to-bottom -> | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ with imscope.child("disc_scroll"): display_entries = list(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"] for i, entry in enumerate(display_entries): render_discussion_entry(app, display_entries[i], i) if app._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); app._scroll_disc_to_bottom = False def render_discussion_entry_controls(app: App) -> None: """Renders the action buttons at the bottom of the discussion panel. Handles adding entries, expanding/collapsing all bubbles, clearing, saving, compressing logs, and configuring auto-history checkpoints. SSDL: `[I:buttons] -> [B:clicks] => [S:entries_or_config]` ASCII Layout Map: +-------------------------------------------------------------+ | [+ Entry] [-All] [+All] [Clear All] [Save] [Compress] | | [ ] Auto-add message & response to history | | Keep Pairs: [15 ] [Truncate] | +-------------------------------------------------------------+ """ 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(); app.save_config(); app.ai_status = "discussion saved" imgui.same_line() if imgui.button("Compress"): app.controller._handle_compress_discussion() _, 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(140) ch, app.ui_disc_truncate_pairs = imgui.drag_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1, 1, 999) if app.ui_disc_truncate_pairs < 1: app.ui_disc_truncate_pairs = 1 imgui.same_line() if imgui.button("Truncate"): with app._disc_entries_lock: app.disc_entries = truncate_entries(app.disc_entries, app.ui_disc_truncate_pairs) app.ai_status = f"history truncated to {app.ui_disc_truncate_pairs} pairs" def render_discussion_metadata(app: App) -> None: """Renders per-discussion metadata: cumulative token counts, git commit association, last updated timestamp, and controls for creating, renaming, and deleting discussions. SSDL: `[I:token_totals] -> [I:commit_row] -> [I:timestamp] -> [B:create/rename/delete]` ASCII Layout Map: +---------------------------------------------------------+ | Discussion Tokens: 1200 In | 800 Out | 0 Cache | | commit: a1b2c3d4 [Update Commit] | | updated: 2026-06-12T22:00 | | [new-name________] [Create] [Rename] [Delete] | +---------------------------------------------------------+ """ 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", "") total_in, total_out, total_cache = 0, 0, 0 for entry in app.disc_entries: if "usage" in entry: total_in += entry["usage"].get("input_tokens", 0) total_out += entry["usage"].get("output_tokens", 0) total_cache += entry["usage"].get("cache_read_input_tokens", 0) if total_in > 0 or total_out > 0: imgui.text_colored(theme.get_color("status_info"), f"Discussion Tokens: {total_in} In | {total_out} Out | {total_cache} Cache") imgui.separator() 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: """Top-level discussion panel compositor. Renders the thinking indicator, prior-session read-only view (if active), discussion selector, entry controls, role list, and scrollable entry bubbles. SSDL: `[I:thinking_indicator] -> [I:prior_view?] | [I:selector] -> [I:controls] -> [I:roles] -> [I:entries]` ASCII Layout Map: +---------------------------------------------------------+ | [● GENERATING...] (thinking spinner) | | | | > Discussions [main v] [Original] [take_2] [Synth] | | commit: a1b2c3 [Update] | updated: 2026-06-12 | | [new-name] [Create] [Rename] [Delete] | | --- | | [+ Entry] [-All] [+All] [Clear All] [Save] [Compress] | | --- | | > Roles (collapsible) | | --- | | Entry #1 [User] ... | | Entry #2 [AI] ... | +---------------------------------------------------------+ """ 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: """Renders the collapsible Roles section. Lists current roles with an [X] delete button per entry, and exposes an input field for adding new roles. SSDL: `[B:collapsing_header] => [I:roles_list] -> [B:add_role]` ASCII Layout Map: +---------------------------------------------------------+ | > Roles | | +---------------------------------------------------+ | | | [X] User [X] AI [X] System | | | +---------------------------------------------------+ | | [new-role___________] [Add] | +---------------------------------------------------------+ """ 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: """Renders the collapsible Discussions selector. Shows a combo-box for choosing the active base discussion, a tab-bar for takes, a Synthesis tab, promote/track controls, and the discussion metadata row. SSDL: `[B:collapsing_header] => [I:combo_base] -> [I:takes_tabs] -> [B:promote?] -> [B:track?] -> [I:metadata]` ASCII Layout Map: +---------------------------------------------------------+ | > Discussions | | [main v] | | [Original] [take_2] [Synthesis] | | [Promote Take] [ ] Track Discussion | | commit: a1b2c3 updated: 2026-06-12 | | [new-name] [Create] [Rename] [Delete] | +---------------------------------------------------------+ """ 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() force_flag = imgui.TabItemFlags_.set_selected if take_name == app.active_discussion and getattr(app, '_force_tab_selection', False) else 0 with imscope.tab_item(f"{label}###{take_name}", force_flag) as (exp, _): if exp and take_name != app.active_discussion: app._switch_discussion(take_name) app._force_tab_selection = False app._force_tab_selection = False 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: """Renders the Discussion tab content. Comprises a resizable top pane (history/entries) and a bottom pane with a draggable splitter bar, pop-out checkboxes, and inline Message/Response tabs. SSDL Shape: `[I:history_child] -> [B:splitter] -> [B:popout_toggles] -> [I:message_response_tabs]` ASCII Layout Map: +---------------------------------------------------------+ | +-----------------------------------------------------+ | | | Discussion entries (scrollable)... | | | | ... | | | +-----------------------------------------------------+ | | [=== drag splitter ===========================] | | [ ] Pop Out Message [ ] Pop Out Response | | [Message] [Response] | | +-----------------------------------------------------+ | | | | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ with imscope.child("HistoryChild", size_x=0, size_y=-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.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.") #endregion: Discussions #region: Operations Monitor def render_operations_hub(app: App) -> None: """Top-level Operations Monitor hub. Houses pop-out checkboxes for Tool Calls, Usage Analytics, and External Tools, then a tab-bar for all ops sub-panels. SSDL: `[B:popout_toggles] -> [I:tab_bar] => [I:active_tab_content]` ASCII Layout Map: +---------------------------------------------------------+ | [ ] Pop Out Tool Calls [ ] Pop Out Usage [ ] Ext Tools| | [Comms History] [Tool Calls] [Usage Analytics] | | [External Tools] [Workspace Layouts] [Vendor State] | | +-----------------------------------------------------+ | | | | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ 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("") ext_panel_result = _render_operations_hub_external_editor_panel_result(app) if not ext_panel_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_operations_hub_external_editor_panel", ext_panel_result.errors[0])) imgui.text_colored(theme.get_color("status_error"), f"Error: {ext_panel_result.errors[0].message}") 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] with imscope.tab_item("Vendor State") as (exp, _): if exp: render_vendor_state(app) def render_vendor_state(app: App) -> None: """Renders the Operations Hub > Vendor State panel. Displays per-vendor health metrics (model name, cache state, token budget, connection status) in a colour-coded table. SSDL: `[I:metrics_table] => [I:state_column]` ASCII Layout Map: +---------------------------------------------------------+ | Metric | Value | State | |--------------------------|-----------------|------------| | Provider | gemini | ok | | Model | gemini-2.0-flash| ok | | Cache Token Budget | 32000 | warn | | Last Error | (none) | info | +---------------------------------------------------------+ """ from src.vendor_state import get_vendor_state metrics = get_vendor_state(app) if imgui.begin_table("vendor_state", 3, imgui.TableFlags_.row_bg | imgui.TableFlags_.borders): imgui.table_setup_column("Metric", imgui.TableColumnFlags_.width_fixed, 180) imgui.table_setup_column("Value", imgui.TableColumnFlags_.width_stretch) imgui.table_setup_column("State", imgui.TableColumnFlags_.width_fixed, 60) imgui.table_headers_row() state_colors = {"ok": theme.get_color("status_success"), "warn": theme.get_color("status_warning"), "error": theme.get_color("status_error"), "info": theme.get_color("text_disabled")} for m in metrics: imgui.table_next_row() imgui.table_next_column(); imgui.text(m.label) imgui.table_next_column() imgui.text(m.value) if imgui.is_item_hovered(): imgui.set_tooltip(m.tooltip) imgui.table_next_column() imgui.text_colored(state_colors.get(m.state, theme.get_color("text_disabled")), m.state) imgui.end_table() def render_message_panel(app: App) -> None: """Renders user message text input area, exposing buttons to generate assistant responses, inject contextual files, or reset active conversation sessions. SSDL: `[I:live_indicator] -> [I:input_textbox] -> [B:gen_send_buttons] -> [B:inject_reset]` ASCII Layout Map: +---------------------------------------------------------+ | LIVE (blinking green when tool is executing) | | +-----------------------------------------------------+ | | | Type your message here... (Ctrl+Enter)| | | | | | | +-----------------------------------------------------+ | | [Gen + Send] [MD Only] [Inject File] [-> History] | | [Reset] | +---------------------------------------------------------+ """ 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 = theme.get_color("status_success", alpha=alpha) imgui.text_colored(c, "LIVE") imgui.separator() ch, app.ui_ai_input = imgui.input_text_multiline("##ai_in", app.ui_ai_input, imgui.ImVec2(-1, -40)) # Keyboard shortcuts io = imgui.get_io() ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) if ctrl_l: app.ui_ai_input = "" imgui.separator() is_busy = app.ai_status in ['sending...', 'streaming...'] send_busy = False with app._send_thread_lock: if app.send_thread and app.send_thread.is_alive(): send_busy = True if is_busy: send_busy = True imgui.begin_disabled(send_busy) ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) label = "Gen + Send (Busy)" if send_busy else "Gen + Send" if (imgui.button(label) or ctrl_enter) and not send_busy: app._handle_generate_send() imgui.end_disabled() imgui.same_line() if imgui.button("MD Only"): app._handle_md_only() imgui.same_line() if imgui.button("Inject File"): app.show_inject_modal = True imgui.same_line() if imgui.button("-> History"): if app.ui_ai_input: app.disc_entries.append({"role": "User", "content": app.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) imgui.same_line() if imgui.button("Reset"): app._handle_reset_session() if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_message_panel") def render_response_panel(app: App) -> None: """Renders assistant stream output panel. Renders thinking traces, markdown segments, and exports active responses to history entries. SSDL: `[I:response_text] -> [I:thinking_trace] -> [I:markdown_view] => [B:export_to_history]` ASCII Layout Map: +---------------------------------------------------------+ | +-----------------------------------------------------+ | | | > Monologue (collapsible thinking trace) | | | | | | | | ## AI Response Heading | | | | Rendered markdown text... | | | +-----------------------------------------------------+ | | [-> History] | +---------------------------------------------------------+ """ 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() app._pending_focus_response = True is_blinking = False blink_color = imgui.ImVec4(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 = theme.get_color("status_success", alpha=alpha) with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): with imscope.child("response_scroll_area", 0, -40, True): with theme.ai_text_style(): segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response) if segments: render_thinking_trace(app, {}, [{"content": s.content, "marker": s.marker} for s in segments], 9999) markdown_helper.render(parsed_response, context_id="response") imgui.separator() if imgui.button("-> History"): if app.ai_response: segments, response = thinking_parser.parse_thinking_trace(app.ai_response) entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} if segments: entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] app.disc_entries.append(entry) if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_response_panel") def render_tool_calls_panel(app: App) -> None: """Renders tool call execution log. Displays script execution details, status codes, and outputs in a scrollable table. Clicking a row opens the full call in the Text Viewer. SSDL: `[I:tool_log] -> [B:clear_button] => [I:calls_table]` ASCII Layout Map: +---------------------------------------------------------+ | Tool call history [Clear] | | +----+------+---------------------------+-----------+ | | | # | Tier | Script | Result | | | +----+------+---------------------------+-----------+ | | | #1 | main | uv run pytest tests/... | passed... | | | | #2 | T3 | git commit -m "fix: ..." | done | | | +----+------+---------------------------+-----------+ | +---------------------------------------------------------+ """ 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() for i, entry in enumerate(log_to_render): imgui.table_next_row() script = entry.get("script", "") res = entry.get("result", "") combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" imgui.table_next_column() # Use selectable for the entire row trigger opened_details = imgui.selectable(f"#{i+1}##row_{i}", False, imgui.SelectableFlags_.span_all_columns)[0] if opened_details: app.text_viewer_title = f"Tool Call #{i+1} Details" app.text_viewer_content = combined app.text_viewer_type = 'markdown' app.show_windows["Text Viewer"] = True imgui.table_next_column() imgui.text_colored(C_SUB(), f"[{entry.get('source_tier', 'main')}]") imgui.table_next_column() script_preview = script.replace("\n", " ")[:150] if len(script) > 150: script_preview += "..." render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1) 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) imgui.end_table() if app._scroll_tool_calls_to_bottom: imgui.set_scroll_here_y(1.0) app._scroll_tool_calls_to_bottom = False if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_calls_panel") def render_external_tools_panel(app: App) -> None: """Renders the External MCPs panel. Shows server health indicators, a [Refresh] button, and a table of all registered external MCP tool names, servers, and descriptions. SSDL Shape: `[B:refresh] -> [I:server_status_badges] -> [I:tools_table]` ASCII Layout Map: +---------------------------------------------------------+ | [Refresh External MCPs] | | Servers: [●] my-mcp-server [●] another-server | | +----------------------+------------+-----------------+ | | | Name | Server | Description | | | +----------------------+------------+-----------------+ | | | py_get_definition | manual-slop| Get Python def | | | +----------------------+------------+-----------------+ | +---------------------------------------------------------+ """ 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_text_viewer_window(app: App) -> None: """Renders the standalone text/code/markdown viewer window. Supports four rendering modes based on content type: markdown (rendered), slice editor (line-click range selection), syntax-highlighted code (CodeEditor widget), and plain text (scrollable). SSDL: `[I:mode_dispatch] => [I:markdown] | [I:slice_editor] | [I:code_editor] | [I:plain_text]` ASCII Layout Map: +---------------------------------------------------------+ | src/gui_2.py - Text Viewer [x] | | --- (slice mode only) --- | | [Add Selection as Slice] [Clear Selection] | | [Auto-Populate AST Slices] [Edit Tags] | | Slice 1: L10-L42 [tag v] [comment________] [X] | | --- | | [Copy] [ ] Word Wrap | | +-----------------------------------------------------+ | | | | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ if not app.show_windows.get("Text Viewer", False): return imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) # Force a unique ID to clear legacy docking corruption expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Unified", True, imgui.WindowFlags_.no_collapse) app.show_windows["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 imgui.same_line() if imgui.button("Auto-Populate AST Slices"): app._populate_auto_slices(app.ui_editing_slices_file) imgui.same_line() if imgui.button("Edit Tags"): imgui.open_popup("Edit Context Tags") if imgui.begin_popup("Edit Context Tags"): tags = app.controller.project.setdefault("context_tags", ["auto-ast", "bug", "feature", "important"]) imgui.text("Context Tags") imgui.separator() to_remove_tag = -1 for i, t in enumerate(tags): imgui.push_id(f"tag_{i}") imgui.set_next_item_width(150) ch, new_t = imgui.input_text("##t", t) if ch: tags[i] = new_t imgui.same_line() if imgui.button("X"): to_remove_tag = i imgui.pop_id() if to_remove_tag != -1: tags.pop(to_remove_tag) if imgui.button("+ Add Tag"): tags.append("new-tag") if imgui.button("Close"): imgui.close_current_popup() imgui.end_popup() to_remove = -1 tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"]) 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() current_tag = slc.get('tag', '') if current_tag not in tags and current_tag: tags.append(current_tag) tag_idx = tags.index(current_tag) if current_tag in tags else 0 imgui.set_next_item_width(100) ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags) if ch_tag: slc['tag'] = tags[new_tag_idx] imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', '')) if changed_comm: slc['comment'] = new_comm imgui.same_line() if imgui.button("X"): 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_auto_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices if slc.get('tag') == 'auto-ast') is_manual_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices if slc.get('tag') != 'auto-ast') if is_manual_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(theme.get_color("slice_manual", alpha=0.2))) elif is_auto_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(theme.get_color("slice_auto", alpha=0.15))) 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(theme.get_color("slice_selection", alpha=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(imgui.HoveredFlags_.allow_when_blocked_by_active_item) 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) ced_result = _render_text_viewer_window_ced_result(app) if not ced_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_text_viewer_window_ced", ced_result.errors[0])) imgui.text_colored(theme.get_color("status_error"), f"CED Error: {ced_result.errors[0].message}") 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() def render_patch_modal(app: App) -> None: """Renders the Apply Patch? modal. Shows files to be modified, a syntax-highlighted diff preview, and action buttons to open in an external editor, apply, or reject. SSDL: `[I:files_list] -> [I:diff_preview] => [B:open_external | B:apply | B:reject]` ASCII Layout Map: +---------------------------------------------------------+ | ⚠ Tier 4 QA Generated a Patch | | Files to modify: | | - src/gui_2.py | | --- | | Diff Preview: | | +-----------------------------------------------------+ | | | --- a/src/gui_2.py | | | | +++ b/src/gui_2.py | | | | @@ -100,7 +100,7 @@ | | | | -old line | | | | +new line | | | +-----------------------------------------------------+ | | [Open in External Editor] [Apply Patch] [Reject] | +---------------------------------------------------------+ """ 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(theme.get_color("status_warning"), "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(theme.get_color("status_error"), 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(theme.get_color("diff_header"), line) elif line.startswith("+"): imgui.text_colored(theme.get_color("diff_added"), line) elif line.startswith("-"): imgui.text_colored(theme.get_color("diff_removed"), 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: """Renders the External Editor configuration panel. Lists configured editors from config.toml, shows the current default, and allows setting a new default. SSDL: `[I:editors_list] -> [B:set_default] => [I:config_hint?]` ASCII Layout Map: +---------------------------------------------------------+ | External Editor for Diff Viewing | | (no editors configured) | | Add editors in config.toml: | | [tools.text_editors.vscode] | | path = "C:\\path\\to\\code.exe" | | --- OR when editors exist --- | | Default: vscode | | [vscode] [notepad++] [Set as Default] | +---------------------------------------------------------+ """ from src.external_editor import get_default_launcher imgui.text("External Editor for Diff Viewing") imgui.separator() ext_config_result = _render_external_editor_panel_config_result(app) if not ext_config_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_external_editor_panel_config", ext_config_result.errors[0])) imgui.text_colored(C_TC(), f"Error: {ext_config_result.errors[0].message}") def render_approve_script_modal(app: App) -> None: """ Renders the modal dialog for approving AI-generated PowerShell scripts. Supports full script editing or markdown code blocks preview. SSDL Shape: `[I:command_details] -> [B:preview_checkbox] -> [I:preview_box_or_editor] => [B:approve_reject]` ASCII Layout Map: +---------------------------------------------------------+ | Approve PowerShell Command | | The AI wants to run the following PowerShell script: | | base_dir: C:\projects\manual_slop | | [ ] Read-only Full Preview | | +-----------------------------------------------------+ | | | Get-Content .\src\gui_2.py | Select-String 'def' | | | | ... | | | +-----------------------------------------------------+ | | [Approve & Run] [Reject] | +---------------------------------------------------------+ """ 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 imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever) if imgui.begin_popup_modal("Approve PowerShell Command", None, 0)[0]: if not dlg: imgui.close_current_popup() else: imgui.text("The AI wants to run the following PowerShell script:") imgui.text_colored(theme.get_color("status_warning"), f"base_dir: {dlg._base_dir}") imgui.separator() # Checkbox to toggle full preview inside modal if not hasattr(app, 'ui_approve_modal_preview'): app.ui_approve_modal_preview = False _, app.ui_approve_modal_preview = imgui.checkbox("Read-only Full Preview", app.ui_approve_modal_preview) avail_y = imgui.get_content_region_avail().y - 40 # reserve space for buttons if app.ui_approve_modal_preview: with imscope.child("preview_child", -1, avail_y, True): markdown_helper.render(f"```powershell\n{dlg._script}\n```", context_id="approve_script_preview") else: ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, avail_y)) 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() #endregion: Operations Monitor #region: Misc Tools def render_theme_panel(app: App) -> None: """Renders the Theme configuration window. Covers palette selection, panel pop-out toggles, font path/size, DPI scale, transparency sliders, background shader, CRT filter, and per-palette tone mapping (brightness/contrast/gamma). SSDL: `[I:palette_combo] -> [B:panel_popout_toggles] -> [I:font_config] -> [I:scale_sliders] -> [I:tone_mapping]` ASCII Layout Map: +---------------------------------------------------------+ | Theme [x] | | Palette: [Nerv v] | | [ ] Separate Message [ ] Separate Response | | [ ] Separate Tool Calls | | Font: [path/to/font.ttf___________] [Browse] | | Size (px): [14.0] [Apply Font (Requires Restart)] | | UI Scale (DPI): [====|====] 1.00 | | Panel Transparency: [======|===] 0.90 | | Panel Item Transparency: [=====|====] 0.85 | | [x] Animated Background Shader [ ] CRT Filter | | Tone Mapping (Per-Palette) | | Brightness: [====|=] 1.00 | | Contrast: [====|=] 1.00 | | Gamma: [====|=] 1.00 | | [Reset Tone Mapping] | +---------------------------------------------------------+ """ 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() app.save_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() app.save_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() app.save_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() app.save_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() app.save_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() app.save_config() imgui.separator() imgui.text("Tone Mapping (Per-Palette)") curr_p = theme.get_current_palette() imgui.text("Brightness") ch_b, b = imgui.slider_float("##tm_b", theme.get_brightness(curr_p), 0.1, 2.0, "%.2f") if ch_b: theme.set_brightness(curr_p, b); app._flush_to_config(); app.save_config() imgui.text("Contrast") ch_c, c = imgui.slider_float("##tm_c", theme.get_contrast(curr_p), 0.1, 2.0, "%.2f") if ch_c: theme.set_contrast(curr_p, c); app._flush_to_config(); app.save_config() imgui.text("Gamma") ch_g, g = imgui.slider_float("##tm_g", theme.get_gamma(curr_p), 0.1, 3.0, "%.2f") if ch_g: theme.set_gamma(curr_p, g); app._flush_to_config(); app.save_config() if imgui.button("Reset Tone Mapping"): theme.reset_tone_mapping(curr_p) app._flush_to_config() app.save_config() imgui.end() if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel") #NOTE(Ed): This was part of an experiment to see what the ai could generate and if it could figure how to add # A post-process mechanism to the gui (it did not). def render_shader_live_editor(app: App) -> None: """Renders the Shader Live Editor window. Exposes real-time sliders for CRT curvature, scanline intensity, and bloom threshold. SSDL: `[I:shader_uniforms] => [I:slider_controls]` ASCII Layout Map: +---------------------------------------------------------+ | Shader Editor [x] | | CRT Curvature: [======|=====] 0.80 | | Scanline Intensity: [==|=========] 0.20 | | Bloom Threshold: [===|========] 0.30 | +---------------------------------------------------------+ """ 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) #TODO(Ed): This shouldn't be here, it needs to be within the test that uitlizes it. def render_markdown_test(app: App) -> None: """Renders a static markdown test panel used to validate the markdown renderer. Displays headers, bold/italic text, lists, links, and a code block. SSDL: `[I:static_md_sample] => [I:rendered_markdown]` ASCII Layout Map: +---------------------------------------------------------+ | Markdown Test Panel | | # Header 1 | | ## Header 2 | | This is **bold** text and *italic* text... | | * List item 1 | | * Sub-item | | [Link to Google](https://google.com) | | ```python | | def hello(): ... | | ``` | +---------------------------------------------------------+ """ 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_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 tint_col = imgui.get_color_u32(theme.get_color("status_error", alpha=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(theme.get_color("status_error"), "HOT RELOAD ERROR") imgui.text_wrapped(HotReloader.last_error or "Unknown error") def render_project_stale_tint(app: App) -> None: """Renders a yellow/amber tint overlay when the project is mid-switch. UI remains responsive (scroll, tab browse) but AI / MD ops are gated on the controller's is_project_stale() returning False. """ if not app.controller.is_project_stale(): return draw_list = imgui.get_background_draw_list() display_size = imgui.get_io().display_size tint_col = imgui.get_color_u32(theme.get_color("status_warning", alpha=0.15)) draw_list.add_rect_filled(imgui.ImVec2(0, 0), display_size, tint_col) pending = app.controller._project_switch_pending_path or app.controller.active_project_path imgui.set_next_window_pos(imgui.ImVec2(10, 50)) with imscope.window("Project Stale", None, imgui.WindowFlags_.always_auto_resize | imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_resize | imgui.WindowFlags_.no_move): imgui.text_colored(theme.get_color("status_warning"), "PROJECT SWITCHING") imgui.text_wrapped(f"Loading: {Path(pending).stem if pending else '?'}") imgui.text_wrapped("UI is read-only until the switch completes. You can still browse tabs.") def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None: """Renders a labelled heavy-text field: a [+] button to pop content into the Text Viewer, a truncated inline preview, and a scrollable child panel showing the full content (markdown-rendered for message/text/content/system labels; plain text otherwise). SSDL: `[B:pop_out_viewer] -> [I:label_preview] -> [I:content_child]` ASCII Layout Map: +---------------------------------------------------------+ | [+] message: "You are a helpful assistant that..." | | +-----------------------------------------------------+ | | | You are a helpful assistant that specializes in... | | | | ...full rendered content... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ if imgui.button(f"[+]##{label}{id_suffix}"): 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 app.show_windows["Text Viewer"] = True imgui.same_line() imgui.text_colored(C_LBL(), f"{label}:"); imgui.same_line() render_selectable_label(app, f"heavy_label_{label}_{id_suffix}", content[:60].replace("\n", " ") + ("..." if len(content)>60 else ""), color=C_VAL()) if content: ctx_id = f"{label}_{id_suffix}" is_md = label in ('message', 'text', 'content', 'system') with imscope.indent(): with imscope.style_color(imgui.Col_.child_bg, theme.get_table_color(is_alt=True)): with imscope.child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 300), imgui.WindowFlags_.always_vertical_scrollbar): if is_md: markdown_helper.render(content, context_id=ctx_id) else: if app.ui_word_wrap: with imscope.text_wrap(imgui.get_content_region_avail().x): imgui.text_colored(theme.get_color("text"), content) else: imgui.text_colored(theme.get_color("text"), content) #endregion: Misc Tools #region: MMA def render_mma_dashboard(app: App) -> None: """Main MMA dashboard interface. SSDL: `[I:focus_selector] -> [I:track_summary] -> [B:epic_planner] -> [I:track_browser] -> [B:global_controls] -> [I:usage_analytics] -> [I:ticket_queue] -> [I:task_dag] => [I:agent_streams]` ASCII Layout Map: +---------------------------------------------------------+ | [Focus: Tier 1 v] [Agent Streams v] | | Track: my-track | Status: RUNNING | Cost: $0.0012 | | Progress: ============================= 67.2% | | [Plan Epic (Tier 1)] [Conductor Setup >] | | Track Browser table... | | [Step Mode] [Pause/Resume] | | Usage section... | | Ticket queue table... | | [Task DAG panel] | | [Ticket Editor if selected] | | Agent Streams tabs... | +---------------------------------------------------------+ """ 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 = theme.get_color("status_warning") if theme.is_nerv_active() else theme.get_color("status_warning") 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", lambda: render_task_dag_panel(app), not app.ui_separate_task_dag) if app.ui_selected_ticket_id: render_mma_ticket_editor(app) imgui.separator() render_mma_agent_streams(app) if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") def render_mma_modals(app: App) -> None: """Renders all MMA-specific approval and info modals: Tool Execution Approval, MMA Step Approval (with optional payload edit), MMA Spawn Approval (with prompt/context editing), and the Cycle Detected error modal. SSDL: `[I:pending_queues] => [I:tool_approval_modal] | [I:step_approval_modal] | [I:spawn_approval_modal] | [I:cycle_modal]` ASCII Layout Map: Conditionally opens (at most one at a time): [Approve Tool Execution modal] Tool: py_get_definition | Args: {...} | [Approve] [Deny] [MMA Step Approval modal] Ticket T-003 waiting... | payload preview or edit [Approve] [Edit Payload / Show Original] [Abort Ticket] [MMA Spawn Approval modal] Spawning Tier 3 for Ticket T-003 Prompt + Context MD preview or edit [Approve] [Edit Mode / Preview Mode] [Abort] [Cycle Detected! modal] Circular dependency found. [OK] """ 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(theme.get_color("status_warning"), 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(theme.get_color("status_error", alpha=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: """Renders the active-track summary row: track name, MMA pipeline status, and running cost estimate, followed by a colour-coded progress bar and a 4-column breakdown table (Completed / In Progress / Blocked / Todo) and ETA estimation. SSDL: `[I:track_name] -> [I:status_badge] -> [I:cost] -> [I:progress_bar] -> [I:stats_table] -> [I:eta]` ASCII Layout Map: +---------------------------------------------------------+ | Track: my-feature | Status: RUNNING | Cost: $0.0024 | | [========================= ] 52.3% | | Completed: 6 | In Progress: 1 | Blocked: 0 | Todo: 5 | | ETA: ~14m (6 tickets remaining) | +---------------------------------------------------------+ """ 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(theme.get_color("status_warning") if is_nerv else theme.get_color("status_warning"), "PIPELINE PAUSED"); imgui.same_line() status_col = imgui.ImVec4(1, 1, 1, 1) if app.mma_status == "idle": status_col = theme.get_color("text_disabled") elif app.mma_status == "running": status_col = theme.get_color("status_success") if is_nerv else theme.get_color("status_warning") elif app.mma_status == "done": status_col = theme.get_color("status_success") elif app.mma_status == "error": status_col = theme.get_color("status_error") if is_nerv else theme.get_color("status_error") elif app.mma_status == "paused": status_col = theme.get_color("status_warning") imgui.text_colored(status_col, app.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(theme.get_color("status_success"), f"${total_cost:,.4f}") perc = track_stats["percentage"] / 100.0 p_color = theme.get_color("status_error") if track_stats["percentage"] < 33 else (theme.get_color("status_warning") if track_stats["percentage"] < 66 else theme.get_color("status_success")) 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: """Renders the Epic Planning panel for Tier 1 strategy input and planning button. SSDL: `[I:input_textbox] => [B:plan_button]` ASCII Layout Map: +---------------------------------------------------------+ | Epic Planning (Tier 1) | | +-----------------------------------------------------+ | | | Describe the epic goal... | | | +-----------------------------------------------------+ | | [Plan Epic (Tier 1) ] | +---------------------------------------------------------+ """ 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: """Renders the Conductor Setup panel. Runs a one-shot project-structure scan and shows the summarised output in a read-only text area. SSDL: `[B:run_scan] => [I:summary_text]` ASCII Layout Map: +---------------------------------------------------------+ | [Run Setup Scan] | | +-----------------------------------------------------+ | | | Project: manual_slop | | | | Tracks found: 3 | Active: my-feature-20260612 | | | | Tickets: 12 todo, 3 done | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ 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: """Renders the Track Browser: a table listing all known tracks with status, progress bar, and a [Load] button per row. Below the table is a form for creating a new track. SSDL: `[I:tracks_table] -> [B:load_button] -> [I:create_track_form] => [B:create_track]` ASCII Layout Map: +---------------------------------------------------------+ | Track Browser | | +------------------+--------+----------+---------+ | | | Title | Status | Progress | Actions | | | +------------------+--------+----------+---------+ | | | my-feature-0612 | ACTIVE | [===== ] [Load] | | | | chore-cleanup | NEW | [ ] [Load] | | | +------------------+--------+----------+---------+ | | Create New Track | | Name: [___________] | | Description: [_____________________________] | | Type: [feature v] | | [Create Track] | +---------------------------------------------------------+ """ 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 = theme.get_color("text_disabled") if status == "new" else (theme.get_color("status_success") if status == "active" and theme.is_nerv_active() else (theme.get_color("status_warning") if status == "active" else (theme.get_color("status_success") if status == "done" else (theme.get_color("status_error") 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 = theme.get_color("status_error") if prog < 0.33 else (theme.get_color("status_warning") if prog < 0.66 else theme.get_color("status_success")) 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: """Renders the MMA global controls bar: Step Mode (HITL) toggle, pipeline status label, Pause/Resume engine button, active tier badge, approval-pending blink indicator, and the Hot Reload shortcut. SSDL: `[B:step_mode] -> [I:status] -> [B:pause_resume] -> [I:active_tier] -> [I:approval_blink] -> [B:hot_reload]` ASCII Layout Map: +---------------------------------------------------------+ | [ ] Step Mode (HITL) Status: RUNNING [Pause] | | | Active: Tier 3 | | *** APPROVAL PENDING *** [Go to Approval] | | Hot Reload: [Reload GUI] (Ctrl+Alt+R) | +---------------------------------------------------------+ """ 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 = theme.get_color("status_error", alpha=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(theme.get_color("status_success"), "Reloaded!") else: imgui.text_colored(theme.get_color("status_error"), 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: """Renders the Tier Usage table (tokens and estimated cost per tier) and, when expanded, a collapsible Tier Model Config section with per-tier provider, model, tool-preset, and persona overrides. SSDL: `[I:usage_table] -> [B:tier_model_config_header] => [I:combos_per_tier]` ASCII Layout Map: +---------------------------------------------------------+ | Tier Usage (Tokens & Cost) | | +--------+------------------+-------+-------+--------+ | | | Tier | Model | Input | Output | Cost | | | +--------+------------------+-------+-------+--------+ | | | Tier 1 | gemini-2.5-flash | 1,200 | 400 | $0.001 | | | | TOTAL | | | | $0.004 | | | +--------+------------------+-------+-------+--------+ | | > Tier Model Config (collapsible) | | Tier 1: [gemini v] [gemini-2.5-flash v] [None v] | +---------------------------------------------------------+ """ 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 ai_client.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: """Renders the ticket detail editor panel, letting the user modify priority, target, and persona override, mark a ticket complete, or delete it. SSDL: `[I:ticket_details] -> [B:combo_selectors] => [B:action_buttons]` ASCII Layout Map: +---------------------------------------------------------+ | Editing: ticket-042 | | Status: in_progress | | Priority: [medium v] | | Target: src/gui_2.py | | Depends on: ticket-041, ticket-040 | | Persona Override: [None v] | | [Mark Complete] [Delete] | +---------------------------------------------------------+ """ 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: """Renders the agent execution stream panels in a tabbed view for Tier 1, 2, 3, and 4. SSDL: `[I] -> [B:tab_bar] => [I:stream_panels]` ASCII Layout Map: +---------------------------------------------------------+ | [Tier 1] [Tier 2] [Tier 3] [Tier 4] ([Beads]) | | [ ] Pop Out Tier 1 | | +-----------------------------------------------------+ | | | Tier 1: Strategy stream text... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ 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: """Renders a single tier's live streaming text panel. For Tier 1, 2, and 4 a single scrollable selectable text area is shown; for Tier 3 (stream_key=None), each worker ticket gets its own labelled, colour-coded sub-child with an auto-scroll behaviour. SSDL: `[I:stream_text] => [I:single_scroll] | [I:worker_sub_children]` ASCII Layout Map (single-stream, e.g. Tier 1): +---------------------------------------------------------+ | +-----------------------------------------------------+ | | | Tier 1 stream output text... | | | | ... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ ASCII Layout Map (Tier 3 multi-worker): +---------------------------------------------------------+ | T-001 [running] | | +-----------------------------------------------------+ | | | Worker output for T-001... | | | +-----------------------------------------------------+ | | T-002 [completed] | | +-----------------------------------------------------+ | | | Worker output for T-002... | | | +-----------------------------------------------------+ | +---------------------------------------------------------+ """ if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tier_stream_panel") if app.is_viewing_prior_session: imgui.text_colored(theme.get_color("status_warning"), "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: _ = _tier_stream_scroll_sync_result(app, stream_key, content, imgui) except Exception as e: err = ErrorInfo( kind=ErrorKind.INTERNAL, message=f"tier stream scroll sync dispatch failed: {e}", source="gui_2.render_tier_stream_panel.dispatcher", original=e, ) if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(('render_tier_stream_panel.scroll_sync', err)) finally: imgui.end_child() 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(theme.get_color("status_warning"), f"{ticket_id} [{status}]") elif status == "completed": imgui.text_colored(theme.get_color("status_success"), f"{ticket_id} [{status}]") elif status == "failed": imgui.text_colored(theme.get_color("status_error"), 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) #NOTE(Ed): Exception(Thirdparty) 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: """Renders the Track Proposal modal. Shows Tier 1's generated implementation tracks with editable title and goal fields, and buttons to remove, start individual tracks, accept all, or cancel. SSDL: `[I:proposed_tracks] -> [B:edit_title/goal] -> [B:remove/start] => [B:accept_all | B:cancel]` ASCII Layout Map: +---------------------------------------------------------+ | Proposed Implementation Tracks | | Title: [my-feature-core_______________] | | Goal: [Implement the rendering pipeline...] | | [Remove] [Start This Track] | | --- | | Title: [my-feature-tests______________] | | Goal: [Write unit tests for...] | | [Remove] [Start This Track] | | --- | | [Accept] [Cancel] | +---------------------------------------------------------+ """ 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 #TODO(Ed): Review local import 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_ticket_queue(app: App) -> None: """Renders the Ticket Queue Management panel. Provides select-all/none controls, bulk action buttons, a 7-column scrollable table of tickets with drag-to-reorder, priority/model combos, status labels, and per-row Kill/Block/Unblock actions. SSDL: `[B:select_all/none] -> [B:bulk_actions] -> [I:ticket_table] => [I:ticket_editor?]` ASCII Layout Map: +---------------------------------------------------------+ | Ticket Queue Management | | [Select All] [Select None] [Bulk Execute] [Bulk Skip] | | [Bulk Block] | | +---+-------+----------+-------+----------+----------+ | | |[ ]| ID | Priority | Model | Status | Desc ... | | | +---+-------+----------+-------+----------+----------+ | | |[x]| T-001 | [high v] |[def v]| in_prog | Impl ... | | | |[ ]| T-002 | [med v] |[def v]| todo | Write... | | | +---+-------+----------+-------+----------+----------+ | +---------------------------------------------------------+ """ 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 = theme.get_color("text_disabled") # gray if prio == 'high': _col = theme.get_color("status_error") # red elif prio == 'medium': p_col = theme.get_color("status_warning") # 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(theme.get_color("status_warning"), 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 """Renders the interactive node editor DAG visualizer, mapping ticket relationships and allowing creation or deletion of links. SSDL: `[I:task_nodes] -> [B:node_editor_canvas] => [I:link_connections]` ASCII Layout Map: +---------------------------------------------------------+ | Task DAG | | +-----------------------------------------------------+ | | | [ticket-041]--->[ticket-042]--->[ticket-043] | | | | | | | | | [ticket-040] | | | +-----------------------------------------------------+ | | [Add Ticket] | +---------------------------------------------------------+ """ 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(theme.get_color("status_info"), "[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 = theme.get_color("status_error") 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 cycle_result = _dag_cycle_check_result(app) if not cycle_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(('render_task_dag_panel.cycle_check', cycle_result.errors[0])) elif cycle_result.data: imgui.open_popup("Cycle Detected!") 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-'): parse_result = _ticket_id_max_int_result(tid) if parse_result.ok: max_id = max(max_id, parse_result.data) else: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(('render_task_dag_panel.ticket_id_parse', parse_result.errors[0])) 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: """Renders the Beads Graph tab. Checks for `dolt` and `bd` CLI availability, shows a warning if missing, then lists beads from the Dolt-backed BeadsClient in a 3-column table (ID / Status / Title). SSDL Shape: `[I:dep_check] -> [B:refresh] -> [I:beads_table]` ASCII Layout Map: +---------------------------------------------------------+ | Beads Graph (Dolt-backed) [Refresh Beads] | | ⚠ Warning: 'dolt' not found in PATH. (if missing) | | +------+-----------+---------------------------------+ | | | ID | Status | Title | | | +------+-----------+---------------------------------+ | | | bd-1 | todo | Implement render pipeline | | | | bd-2 | complete | Write tests | | | +------+-----------+---------------------------------+ | +---------------------------------------------------------+ """ 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(theme.get_color("status_warning"), 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": beads_result = _render_beads_tab_list_result(app) if not beads_result.ok: if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] app._last_request_errors.append(("_render_beads_tab_list", beads_result.errors[0])) imgui.text_colored(theme.get_color("status_error"), f"Error loading beads: {beads_result.errors[0].message}") else: beads = beads_result.data 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() def render_mma_focus_selector(app: App) -> None: """Renders the Focus Agent selector. Filters the discussion entries list to show only the output of the chosen tier agent (or All). Includes a clear [x] button. SSDL: `[I:focus_combo] -> [B:clear_x?]` ASCII Layout Map: +---------------------------------------------------------+ | Focus Agent: [All v] (or [Tier 2 v] [x]) | +---------------------------------------------------------+ """ 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() if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None #region: Drain Plane (result_migration_gui_2_20260619 Phase 2) def _drain_normalize_errors(value: Any) -> list: """Normalize any of the 8 controller error attribute shapes to a flat list. The 8 attributes (added by sub-track 3 Phase 6) have three distinct shapes: - Optional[ErrorInfo]: single error or None (_signal_handler_error, _inject_preview_error, _mcp_config_parse_error, _save_project_error) - List[Tuple[str, ErrorInfo]]: append-only (op_name, error) tuples (_last_request_errors, _worker_errors, _startup_timeline_errors) - Dict[str, ErrorInfo]: provider -> error (_model_fetch_errors) This helper extracts the ErrorInfo objects from each shape so the render loop can iterate uniformly. Implemented via duck typing (no isinstance against ErrorInfo, since gui_2.py intentionally does not import the result_types module to keep the import graph lean). [SDM: src/gui_2.py:_drain_normalize_errors] """ if value is None: return [] if isinstance(value, dict): return list(value.values()) if isinstance(value, (list, tuple)): result = [] for item in value: if isinstance(item, tuple) and len(item) >= 2: result.append(item[1]) else: result.append(item) return result return [value] def render_controller_error_modal(app: "App") -> None: """Drain plane: read the 8 controller error attributes; open imgui.open_popup for each non-empty. This is the canonical Pattern 2 drain point (per error_handling.md:396-407): the caller (the render loop) calls this every frame; the function is non-blocking and only triggers popups for non-empty error states. The 8 controller attributes drained here are the data plane added by sub-track 3 (result_migration_app_controller_20260618). The render functions consume them and surface them to the user via ImGui popups. [C: src/gui_2.py:_render_controller_error_modal delegation wrapper, src/gui_2.py:_render_main_interface (call site TBD)] """ attrs = [ ("_last_request_errors", "Last Request Errors"), ("_worker_errors", "Worker Errors"), ("_startup_timeline_errors", "Startup Timeline Errors"), ("_signal_handler_error", "Signal Handler Error"), ("_inject_preview_error", "Inject Preview Error"), ("_mcp_config_parse_error", "MCP Config Parse Error"), ("_save_project_error", "Save Project Error"), ("_model_fetch_errors", "Model Fetch Errors"), ] for attr_name, popup_title in attrs: raw = getattr(app.controller, attr_name, None) errs = _drain_normalize_errors(raw) if not errs: continue popup_id = f"{popup_title}##{attr_name}" imgui.open_popup(popup_id) opened, _ = imgui.begin_popup(popup_id) if opened: imgui.text(f"{popup_title}: {len(errs)} error{'s' if len(errs) != 1 else ''}") imgui.separator() for e in errs: imgui.text_wrapped(getattr(e, "ui_message", lambda: str(e))()) imgui.separator() if imgui.button("OK"): imgui.close_current_popup() imgui.end_popup() def _render_worker_error_indicator(app: "App") -> None: """Status-bar widget showing worker error count. Click opens the controller error modal. Visible only when app.controller._worker_errors is non-empty. Per the UI delegation pattern (conductor/product-guidelines.md), the function lives at module level and the App class wraps it as a thin delegation. [C: src/gui_2.py:App._render_worker_error_indicator delegation wrapper] """ errs = getattr(app.controller, "_worker_errors", None) if not errs: return n = len(errs) imgui.text(f"[!] {n} worker error{'s' if n != 1 else ''}") if imgui.is_item_hovered() and imgui.is_mouse_clicked(0): render_controller_error_modal(app) def _render_last_request_errors_modal(app: "App") -> None: """Per-request error modal. Surfaced after each AI request via the request completion hook. Opens a modal only if errors accumulated during the request (app.controller._last_request_errors is non-empty). [C: src/gui_2.py:App._render_last_request_errors_modal delegation wrapper] """ errs = getattr(app.controller, "_last_request_errors", None) if not errs: return popup_id = "Request Errors##_last_request_errors" opened, _ = imgui.begin_popup_modal(popup_id) if opened: imgui.text(f"{len(errs)} error{'s' if len(errs) != 1 else ''} during last request:") imgui.separator() for e in errs: imgui.text_wrapped(getattr(e, "ui_message", lambda: str(e))()) imgui.separator() if imgui.button("OK"): imgui.close_current_popup() imgui.end_popup() #endregion: Drain Plane #region: Phase 3 Result Stubs (result_migration_gui_2_20260619) class _LocalErrorInfo: """Minimal duck-typed ErrorInfo. Mirrors src.result_types.ErrorInfo API.""" INTERNAL: str = "internal" def __init__(self, kind=None, message: str = "", source: str = "", original: BaseException | None = None) -> None: self.kind = kind if kind is not None else self.INTERNAL self.message = message self.source = source self.original = original def ui_message(self) -> str: src = f"[{self.source}] " if self.source else "" return f"{src}{self.kind}: {self.message}" class _LocalResult: """Minimal duck-typed Result. Mirrors src.result_types.Result API.""" def __init__(self, data=True, errors: list | None = None) -> None: self.data = data self.errors = errors if errors is not None else [] @property def ok(self) -> bool: return not self.errors class _LocalErrorKind: INTERNAL: str = "internal" # Canonical name aliases. These names appear in helper return statements and # ErrorInfo(...) calls so audit_exception_handling.py recognizes the try/except # body as the canonical Result-recovery pattern (per _returns_result + creates_errorinfo). Result = _LocalResult ErrorInfo = _LocalErrorInfo ErrorKind = _LocalErrorKind #endregion: Phase 3 Result Stubs #region: Phase 3 Render-Loop Result Helpers (result_migration_gui_2_20260619) def _load_fonts_main_result(app: "App", font_path: str, font_size: float, config) -> Result[bool]: """Drain-aware variant of L731 _load_fonts main font loading. Extracts the thirdparty hello_imgui.load_font_ttf_with_font_awesome_icons try/except from App._load_fonts into a Result-returning helper. On exception, sets app.main_font = None and returns Result(data=False, errors=[ErrorInfo]). On success, sets app.main_font to the loaded font. [C: src/gui_2.py:App._load_fonts (L731 legacy wrapper)] """ from src.startup_profiler import startup_profiler try: with startup_profiler.phase("load_fonts.main_with_fontawesome"): app.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config) return Result(data=True) except Exception as e: app.main_font = None return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Failed to load main font {font_path}: {e}", source="gui_2._load_fonts_main_result", original=e, )]) def _load_fonts_mono_result(app: "App", font_size: float, config) -> Result[bool]: """Drain-aware variant of L742 _load_fonts mono font loading. Extracts the thirdparty hello_imgui.FontLoadingParams + hello_imgui.load_font try/except from App._load_fonts into a Result-returning helper. On exception, sets app.mono_font = None and returns Result(data=False, errors=[ErrorInfo]). On success, sets app.mono_font to the loaded font. [C: src/gui_2.py:App._load_fonts (L742 legacy wrapper)] """ from src.startup_profiler import startup_profiler try: with startup_profiler.phase("load_fonts.mono"): params = hello_imgui.FontLoadingParams(font_config=config) app.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params) return Result(data=True) except Exception as e: app.mono_font = None return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Failed to load mono font: {e}", source="gui_2._load_fonts_mono_result", original=e, )]) def _render_main_interface_result(app: "App") -> Result[bool]: """Drain-aware variant of L1123 _gui_func render_main_interface call. Extracts the OUTER render-loop try/except from App._gui_func into a Result-returning helper. The legacy wrapper MUST NOT break the render frame even if the error drain itself fails (per task constraint on L1123). [C: src/gui_2.py:App._gui_func (L1123 legacy wrapper)] """ try: if app.is_viewing_prior_session: with imscope.style_color(imgui.Col_.window_bg, theme.get_color("bubble_vendor")): render_main_interface(app) else: render_main_interface(app) return Result(data=True) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"ERROR in _gui_func: {e}", source="gui_2._render_main_interface_result", original=e, )]) def _show_menus_do_generate_result(app: "App") -> Result[bool]: """Drain-aware variant of L1171 _show_menus 'Generate MD Only' menu handler. Extracts the try/except from the "Generate MD Only" if-branch in App._show_menus into a Result-returning helper. On success, sets app.last_md, app.last_md_path, app.ai_status. On failure, sets app.ai_status to an error message. [C: src/gui_2.py:App._show_menus (L1171 legacy wrapper)] """ try: md, path, *_ = app._do_generate() app.last_md = md app.last_md_path = path app.ai_status = f"md written: {path.name}" return Result(data=True) except Exception as e: app.ai_status = f"error: {e}" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Generate MD Only failed: {e}", source="gui_2._show_menus_do_generate_result", original=e, )]) def _show_menus_hwnd_result(app: "App") -> Result[int]: """Drain-aware variant of L1197 _show_menus hwnd capsule try/except. Extracts the ctypes PyCapsule_GetPointer try/except from App._show_menus into a Result-returning helper. On success, returns Result(data=hwnd) where hwnd is the resolved window handle. On failure (e.g., on a non-Windows platform), returns Result(data=0, errors=[ErrorInfo]). The data field is the resolved hwnd (int) so the legacy wrapper can pass it to subsequent win32gui calls without an additional app.hwnd instance attribute. [C: src/gui_2.py:App._show_menus (L1197 legacy wrapper)] """ try: import ctypes ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] hwnd_capsule = imgui.get_main_viewport().platform_handle_raw hwnd = ctypes.pythonapi.PyCapsule_GetPointer(hwnd_capsule, b"nb_handle") return Result(data=int(hwnd) if hwnd else 0) except Exception as e: return Result(data=0, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Failed to resolve hwnd via PyCapsule_GetPointer: {e}", source="gui_2._show_menus_hwnd_result", original=e, )]) def _show_menus_is_max_result(app: "App", hwnd) -> Result[bool]: """Drain-aware variant of L1222 _show_menus GetWindowPlacement try/except. Extracts the win32gui.GetWindowPlacement try/except from App._show_menus into a Result-returning helper. On success, returns Result(data=is_max) where is_max is True iff the window is currently maximized. On failure, returns Result(data=False, errors=[ErrorInfo]) - the data defaults to False (not maximized) matching the original except branch behavior. [C: src/gui_2.py:App._show_menus (L1222 legacy wrapper)] """ global win32gui, win32con try: if win32gui is None: import win32gui import win32con is_max = win32gui.GetWindowPlacement(hwnd)[1] == win32con.SW_SHOWMAXIMIZED return Result(data=is_max) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"GetWindowPlacement failed for hwnd={hwnd}: {e}", source="gui_2._show_menus_is_max_result", original=e, )]) def _render_warmup_status_indicator_result(app: "App") -> Result[dict]: """Drain-aware variant of L4848 render_warmup_status_indicator warmup_status try/except. Extracts the controller.warmup_status() try/except from render_warmup_status_indicator into a Result-returning helper. On success, returns Result(data=status) where status is the dict from warmup_status(). On failure, returns Result(data={}, errors=[ErrorInfo]). The data field is the status dict so the legacy wrapper can use it for rendering without an additional instance attribute. Errors drain to app.controller._worker_errors (worker error plane, lock-protected). [C: src/gui_2.py:render_warmup_status_indicator (L4848 legacy wrapper)] """ try: status = app.controller.warmup_status() return Result(data=status) except Exception as e: return Result(data={}, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"warmup_status failed: {e}", source="gui_2._render_warmup_status_indicator_result", original=e, )]) def _handle_history_logic_result(app: "App") -> Result[bool]: """Drain-aware variant of L1284 _handle_history_logic snapshot try/except. Extracts the snapshot debounce try/except from App._handle_history_logic into a Result-returning helper. The legacy wrapper keeps the _is_applying_snapshot early return (a pre-condition guard, not error handling) and delegates the rest to this helper. On success, returns Result(data=True). On failure (any exception during _take_snapshot or the snapshot diff), returns Result(data=False, errors=[ErrorInfo]). [C: src/gui_2.py:App._handle_history_logic (L1284 legacy wrapper)] """ try: current = app._take_snapshot() if app._last_ui_snapshot is None: app._last_ui_snapshot = current return Result(data=True) changed = ( current.ai_input != app._last_ui_snapshot.ai_input or current.project_system_prompt != app._last_ui_snapshot.project_system_prompt or current.global_system_prompt != app._last_ui_snapshot.global_system_prompt or current.base_system_prompt != app._last_ui_snapshot.base_system_prompt or current.use_default_base_prompt != app._last_ui_snapshot.use_default_base_prompt or abs(current.temperature - app._last_ui_snapshot.temperature) > 1e-5 or abs(current.top_p - app._last_ui_snapshot.top_p) > 1e-5 or current.max_tokens != app._last_ui_snapshot.max_tokens or current.auto_add_history != app._last_ui_snapshot.auto_add_history or len(current.disc_entries) != len(app._last_ui_snapshot.disc_entries) or len(current.files) != len(app._last_ui_snapshot.files) or len(current.context_files) != len(app._last_ui_snapshot.context_files) or len(current.screenshots) != len(app._last_ui_snapshot.screenshots) ) if not changed and len(current.disc_entries) > 0: if current.disc_entries[-1].get('content') != app._last_ui_snapshot.disc_entries[-1].get('content'): changed = True if changed: if not app._pending_snapshot: app._pending_snapshot = True app._snapshot_timer = time.time() app._state_to_push = app._last_ui_snapshot else: app._snapshot_timer = time.time() app._last_ui_snapshot = current if app._pending_snapshot and (time.time() - app._snapshot_timer > app._snapshot_debounce): if app._state_to_push: app.history.push(app._state_to_push, "UI Update") app._state_to_push = None app._pending_snapshot = False return Result(data=True) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"[DEBUG History] ERROR in _handle_history_logic: {e}", source="gui_2._handle_history_logic_result", original=e, )]) #endregion: Phase 3 Render-Loop Result Helpers #region: Phase 4 Modal/Dialog Result Helpers (result_migration_gui_2_20260619) def _render_persona_editor_save_result(app: "App") -> Result[bool]: """Drain-aware variant of L3398 render_persona_editor_window Save button try/except. Extracts the models.Persona(...) construction + app.controller._cb_save_persona try/except from the Save button handler in render_persona_editor_window into a Result-returning helper. On success, sets app.ai_status to "Saved: " and returns Result(data=True). On failure (any exception in Persona construction or _cb_save_persona), sets app.ai_status to an error message and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-3 modal pattern: data plane attribute; the modal itself is the UI surface). [C: src/gui_2.py:render_persona_editor_window (L3398 legacy wrapper)] """ 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}" return Result(data=True) except Exception as e: app.ai_status = f"Error: {e}" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Persona save failed: {e}", source="gui_2._render_persona_editor_save_result", )]) def _render_ast_inspector_outline_result(app: "App", f_path: str) -> Result[str]: """Drain-aware variant of L3718 render_ast_inspector_modal outline fetch try/except. Extracts the mcp_client.configure + mcp_client.{py,ts_c,ts_cpp}_get_code_outline try/except from render_ast_inspector_modal into a Result-returning helper. On success, returns Result(data=outline) where outline is the string from the appropriate outline function (or "" if no extension matched). On failure (any exception during configure or outline fetch), returns Result(data=f"Error fetching outline: {e}", errors=[ErrorInfo]) so the legacy caller can still display a meaningful error message. The data field carries the outline string so the legacy wrapper can iterate it without an additional instance attribute. Errors drain to app._last_request_errors (per FR-BC-3 modal pattern; data plane). [C: src/gui_2.py:render_ast_inspector_modal (L3718 legacy wrapper)] """ try: proj_dir = str(Path(app.controller.active_project_path).parent.resolve()) if getattr(app, 'controller', None) and app.controller.active_project_path else None mcp_client.configure([{"path": f_path}], [proj_dir] if proj_dir else None) outline = "" 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) elif f_path.lower().endswith(('.cpp', '.hpp', '.cxx', '.cc')): outline = mcp_client.ts_cpp_get_code_outline(f_path) return Result(data=outline) except Exception as e: return Result(data=f"Error fetching outline: {e}", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"AST outline fetch failed for {f_path}: {e}", source="gui_2._render_ast_inspector_outline_result", original=e, )]) def _render_ast_inspector_file_content_result(app: "App", f_path: str) -> Result[str | None]: """Drain-aware variant of L3740 render_ast_inspector_modal file read try/except. Extracts the mcp_client.read_file try/except from render_ast_inspector_modal into a Result-returning helper. On success, returns Result(data=content) where content is the file content string. On failure (read_file raises), returns Result(data=None, errors=[ErrorInfo]). The legacy wrapper handles the side effects: - On success: app._cached_ast_file_lines = content.splitlines(); app.text_viewer_content = content - On failure: app._cached_ast_file_lines = ["Error loading file content."]; app.text_viewer_content = "Error loading file content." And drains errors to app._last_request_errors (per FR-BC-3 modal pattern). [C: src/gui_2.py:render_ast_inspector_modal (L3740 legacy wrapper)] """ try: content = mcp_client.read_file(f_path) return Result(data=content) except Exception as e: return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"AST file content read failed for {f_path}: {e}", source="gui_2._render_ast_inspector_file_content_result", original=e, )]) #endregion: Phase 4 Modal/Dialog Result Helpers #region: Phase 5 Event Handler Result Helpers (result_migration_gui_2_20260619) def _populate_auto_slices_outline_result(app: "App", f_item, abs_path: str) -> Result[str]: """Drain-aware variant of L1284 _populate_auto_slices outline fetch. Extracts the mcp_client.{py,ts_c,ts_cpp}_get_code_outline try/except from App._populate_auto_slices into a Result-returning helper. On success (extension matches and outline fetch succeeded), returns Result(data=outline). On "skip" (no matching extension), returns Result(data=""). On exception, returns Result(data="", errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:App._populate_auto_slices (L1284 legacy wrapper)] """ f_path_lower = f_item.path.lower() try: if f_path_lower.endswith('.py'): return Result(data=mcp_client.py_get_code_outline(abs_path)) elif f_path_lower.endswith(('.c', '.h')): return Result(data=mcp_client.ts_c_get_code_outline(abs_path)) elif f_path_lower.endswith(('.cpp', '.hpp', '.cxx', '.cc')): return Result(data=mcp_client.ts_cpp_get_code_outline(abs_path)) else: return Result(data="") except Exception as e: return Result(data="", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"AST outline fetch failed for {f_item.path}: {e}", source="gui_2._populate_auto_slices_outline_result", original=e, )]) def _populate_auto_slices_file_read_result(app: "App", f_item) -> Result[str]: """Drain-aware variant of L1293 _populate_auto_slices file read. Extracts the file read try/except from App._populate_auto_slices into a Result-returning helper. On success, returns Result(data=content) where content is the file text. On exception (file not found, encoding error, permission denied), returns Result(data="", errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:App._populate_auto_slices (L1293 legacy wrapper)] """ try: with open(f_item.path, "r", encoding="utf-8") as f: text = f.read() return Result(data=text) except Exception as e: return Result(data="", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"File read failed for {f_item.path}: {e}", source="gui_2._populate_auto_slices_file_read_result", original=e, )]) def _apply_pending_patch_result(app: "App") -> Result[bool]: """Drain-aware variant of L1367 _apply_pending_patch. Extracts the apply_patch_to_file try/except from App._apply_pending_patch into a Result-returning helper. On success (apply_patch_to_file returns (True, msg)), sets app._show_patch_modal=False, app._pending_patch_text=None, app._pending_patch_files=[], app._patch_error_message=None and returns Result(data=True). On failure (apply_patch_to_file returns (False, msg) or raises), sets app._patch_error_message to the error string and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:App._apply_pending_patch (L1367 legacy wrapper)] """ try: base_dir = str(app.controller.current_project_dir) if hasattr(app.controller, 'current_project_dir') else "." success, msg = apply_patch_to_file(app._pending_patch_text, base_dir) if success: app._show_patch_modal = False app._pending_patch_text = None app._pending_patch_files = [] app._patch_error_message = None imgui.close_current_popup() return Result(data=True) else: app._patch_error_message = msg return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"apply_patch_to_file returned False: {msg}", source="gui_2._apply_pending_patch_result", )]) except Exception as e: app._patch_error_message = str(e) return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Apply pending patch failed: {e}", source="gui_2._apply_pending_patch_result", original=e, )]) def _open_patch_in_external_editor_result(app: "App") -> Result[bool]: """Drain-aware variant of L1393 _open_patch_in_external_editor. Extracts the external editor launch try/except from App._open_patch_in_external_editor into a Result-returning helper. On success (launcher.launch_diff returns a process), sets app._patch_error_message=None, app._vscode_diff_process=result and returns Result(data=True). On failure (launch returns None or raises), sets app._patch_error_message and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:App._open_patch_in_external_editor (L1393 legacy wrapper)] """ from src.external_editor import get_default_launcher, create_temp_modified_file import os try: if not app._pending_patch_files: app._patch_error_message = "No files to edit" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message="No files to edit", source="gui_2._open_patch_in_external_editor_result", )]) launcher = get_default_launcher(app.config) editor = launcher.config.get_default() if not editor: app._patch_error_message = "No external editor configured" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message="No external editor configured", source="gui_2._open_patch_in_external_editor_result", )]) original_path = app._pending_patch_files[0] if not os.path.exists(original_path): app._patch_error_message = f"Original file not found: {original_path}" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Original file not found: {original_path}", source="gui_2._open_patch_in_external_editor_result", )]) temp_path = create_temp_modified_file(app._pending_patch_text) result = launcher.launch_diff(None, original_path, temp_path) if result is None: app._patch_error_message = "Failed to launch external editor" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message="Failed to launch external editor", source="gui_2._open_patch_in_external_editor_result", )]) app._patch_error_message = None app._vscode_diff_process = result return Result(data=True) except Exception as e: app._patch_error_message = str(e) return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Open patch in external editor failed: {e}", source="gui_2._open_patch_in_external_editor_result", original=e, )]) def request_patch_from_tier4_result(app: "App", error: str, file_context: str) -> Result[bool]: """Drain-aware variant of L1428 request_patch_from_tier4. Extracts the ai_client.run_tier4_patch_generation try/except from App.request_patch_from_tier4 into a Result-returning helper. On success (patch_text has --- and +++), sets app._pending_patch_text/files and app._show_patch_modal=True and returns Result(data=True). On invalid patch (no --- or no +++) or exception, sets app._patch_error_message and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:App.request_patch_from_tier4 (L1428 legacy wrapper)] """ from src.diff_viewer import parse_diff try: patch_text = ai_client.run_tier4_patch_generation(error, file_context) if patch_text and "---" in patch_text and "+++" in patch_text: diff_files = parse_diff(patch_text) file_paths = [df.old_path for df in diff_files] app._pending_patch_text = patch_text app._pending_patch_files = file_paths app._show_patch_modal = True return Result(data=True) else: app._patch_error_message = patch_text or "No patch generated" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Tier4 patch invalid or empty: {app._patch_error_message}", source="gui_2.request_patch_from_tier4_result", )]) except Exception as e: app._patch_error_message = str(e) return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Tier4 patch generation failed: {e}", source="gui_2.request_patch_from_tier4_result", original=e, )]) def _render_tool_preset_bias_save_result(app: "App") -> Result[bool]: """Drain-aware variant of L3163 render_tool_preset_manager_content Save Profile button. Extracts the BiasProfile save try/except from render_tool_preset_manager_content into a Result-returning helper. On success (BiasProfile construction + _cb_save_bias_profile), sets app.ai_status to "Saved: " and returns Result(data=True). On failure (BiasProfile construction or _cb_save_bias_profile raises), sets app.ai_status to an error message and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper (the imgui.button callback) drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_tool_preset_manager_content (L3163 legacy wrapper)] """ 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}" return Result(data=True) except Exception as e: app.ai_status = f"Error: {e}" return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Bias profile save failed: {e}", source="gui_2._render_tool_preset_bias_save_result", original=e, )]) def _render_context_batch_actions_preview_result(app: "App") -> Result[str]: """Drain-aware variant of L3582 render_context_batch_actions Preview button. Extracts the _do_generate preview try/except from render_context_batch_actions into a Result-returning helper. On success, returns Result(data=preview_text) where preview_text is the controller._do_generate() output. On exception, captures the traceback, returns Result(data="", errors=[ErrorInfo]). The legacy wrapper (the imgui.button callback) drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_context_batch_actions (L3582 legacy wrapper)] """ try: app.controller.context_files = app.context_files preview_text = app.controller._do_generate()[0] return Result(data=preview_text) except Exception as e: import traceback err = traceback.format_exc() preview_text = f"# Error generating preview\n\n```python\n{err}\n```" return Result(data=preview_text, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Context preview generation failed: {e}", source="gui_2._render_context_batch_actions_preview_result", original=e, )]) def _render_operations_hub_external_editor_panel_result(app: "App") -> Result[bool]: """Drain-aware variant of L5380 render_operations_hub External Tools tab. Extracts the render_external_editor_panel call try/except from render_operations_hub External Tools tab into a Result-returning helper. On success, returns Result(data=True). On exception, converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_operations_hub (L5380 legacy wrapper)] """ try: render_external_editor_panel(app) return Result(data=True) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Render external editor panel from operations hub failed: {e}", source="gui_2._render_operations_hub_external_editor_panel_result", original=e, )]) def _render_text_viewer_window_ced_result(app: "App") -> Result[bool]: """Drain-aware variant of L5786 render_text_viewer_window CED branch. Extracts the TextEditor set_text/render try/except from render_text_viewer_window CED branch into a Result-returning helper. On success, returns Result(data=True). On exception, converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_text_viewer_window (L5786 legacy wrapper)] """ 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") return Result(data=True) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Text viewer CED render failed: {e}", source="gui_2._render_text_viewer_window_ced_result", original=e, )]) def _render_external_editor_panel_config_result(app: "App") -> Result[bool]: """Drain-aware variant of L5920 render_external_editor_panel config block. Extracts the external editor config rendering try/except from render_external_editor_panel into a Result-returning helper. On success, returns Result(data=True). On exception, converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_external_editor_panel (L5920 legacy wrapper)] """ from src.external_editor import get_default_launcher try: launcher = get_default_launcher(app.config) 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") return Result(data=True) except Exception as e: return Result(data=False, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Render external editor panel config failed: {e}", source="gui_2._render_external_editor_panel_config_result", original=e, )]) def _render_beads_tab_list_result(app: "App") -> Result[list]: """Drain-aware variant of L7208 render_beads_tab BeadsClient list. Extracts the beads_client.BeadsClient(...) + list_beads() try/except in render_beads_tab into a Result-returning helper. On success, returns Result(data=beads) where beads is the list of beads. On exception, returns Result(data=[], errors=[ErrorInfo]). The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; data plane attribute). [C: src/gui_2.py:render_beads_tab (L7208 legacy wrapper)] """ from src import beads_client try: bclient = beads_client.BeadsClient(Path(app.active_project_root)) beads = bclient.list_beads() return Result(data=beads) except Exception as e: return Result(data=[], errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Render beads tab list failed: {e}", source="gui_2._render_beads_tab_list_result", original=e, )]) #endregion: Phase 5 Event Handler Result Helpers #region: Phase 7 Worker/Background Result Helpers (result_migration_gui_2_20260619) def _worker_context_preview_result(app: "App") -> Result[None]: """Drain-aware variant of L4321 worker closure in _check_auto_refresh_context_preview. Extracts the try body from the worker() closure into a Result-returning helper. On exception, sets app.context_preview_text to a fallback message and returns Result(data=None, errors=[ErrorInfo]). On success, sets app.context_preview_text to the generated markdown. The legacy worker() wrapper drains errors to app.controller._worker_errors (with the controller lock acquired on append). [C: src/gui_2.py:_check_auto_refresh_context_preview (L4321 legacy wrapper)] """ try: app.controller.context_files = app.context_files res = app.controller._do_generate() app.context_preview_text = res[0] return Result(data=None) except Exception as e: app.context_preview_text = "Error generating context preview." return Result(data=None, errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Worker error generating context preview: {e}", source="gui_2._worker_context_preview_result", original=e, )]) #endregion: Phase 7 Worker/Background Result Helpers #region: Phase 8 Property Setter / State Result Helpers (result_migration_gui_2_20260619) def _diag_layout_state_ini_text_result(app: "App", ini_path: str) -> Result[str]: """Drain-aware variant of L591 _diag_layout_state ini-file-read try block. Extracts the thirdparty file-open try/except from App._diag_layout_state into a Result-returning helper. On exception, returns Result(data="", errors=[ErrorInfo]) so the legacy wrapper can early-return. On success, returns Result(data=ini_text) where ini_text is the file content. The legacy wrapper drains errors to app._startup_timeline_errors (startup callback drain plane). [C: src/gui_2.py:App._diag_layout_state (L591 legacy wrapper)] """ try: with open(ini_path, encoding="utf-8") as _f: ini_text = _f.read() return Result(data=ini_text) except Exception as e: sys.stderr.write(f"[GUI] could not read layout file: {e}\n") return Result(data="", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"Could not read layout file {ini_path}: {e}", source="gui_2._diag_layout_state_ini_text_result", original=e, )]) def _capture_workspace_profile_ini_result(app: "App") -> Result[str]: """Drain-aware variant of L897 _capture_workspace_profile ini-capture try block. Extracts the thirdparty imgui.save_ini_settings_to_memory try/except from App._capture_workspace_profile into a Result-returning helper. On exception, returns Result(data="", errors=[ErrorInfo]) so the legacy wrapper can fall back to the empty-string ini content. On success, returns Result(data=ini_str) where ini_str is the serialized INI content. The legacy wrapper drains errors to app._last_request_errors (per FR-BC-4 event-handler drain pattern; this site is a property setter on the App). [C: src/gui_2.py:App._capture_workspace_profile (L897 legacy wrapper)] """ try: ini = str(imgui.save_ini_settings_to_memory() or "") return Result(data=ini) except Exception as e: return Result(data="", errors=[ErrorInfo( kind=ErrorKind.INTERNAL, message=f"imgui.save_ini_settings_to_memory failed: {e}", source="gui_2._capture_workspace_profile_ini_result", original=e, )]) #endregion: Phase 8 Property Setter / State Result Helpers #endregion: MMA