Private
Public Access
0
0
Files
manual_slop/src/gui_2.py
T
ed f0ae074aec fix(gui_2): restore _last_imgui_assert as string (regression from Phase 10)
The Phase 10 migration of the run() function (L728 INTERNAL_SILENT_SWALLOW)
changed App.run's error drain to set self.controller._last_imgui_assert
to traceback.format_exception(...), which returns a list. But the
existing test test_app_run_imgui_assert_handling.py expects it to be
a string containing 'Missing End'.

Fix: set _last_imgui_assert to str(err.original) if available, else
err.message. The IM_ASSERT message string is what the health endpoint
expects.

TIER-2 READ conductor/code_styleguides/error_handling.md end-to-end before Phase 13.

Regression test: tests/test_app_run_imgui_assert_handling.py
test_app_run_records_degraded_state_on_imgui_assert PASSES after fix.
2026-06-20 02:39:47 -04:00

8452 lines
398 KiB
Python

# 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.<field> 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] |
| +-----------------------------------------------------+ |
| | <active tab content> | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
"""
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] |
| +-----------------------------------------------------+ |
| | <active tab content> | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
"""
_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: <click to expand>] |
| |
| "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] |
| +-----------------------------------------------------+ |
| | <active tab panel> | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
"""
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] |
| +-----------------------------------------------------+ |
| | <active tab content> | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
"""
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 |
| +-----------------------------------------------------+ |
| | <markdown / slice-editor / CodeEditor / plain text> | |
| +-----------------------------------------------------+ |
+---------------------------------------------------------+
"""
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: <name>"
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: <name>" 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="<error markdown>", 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