f3d071e0c8
Sub-track 4 of startup_speedup_20260606. Adds per-frame GUI feedback during the AppController's background warmup: - render_warmup_status_indicator(app): module-level render fn called from render_main_interface. Shows 'Warming up... (N/M)' in warning color while pending, 'Imports: K failed' in error color on failure, or 'All imports ready (M modules)' in success color for 3 seconds after completion. Hidden otherwise. - _on_warmup_complete_callback(app, status): thread-safe callback registered with controller.on_warmup_complete() in App._post_init. Records timestamp + lock-protected toast list. - App._post_init: registers the callback. 6 new tests in tests/test_gui_warmup_indicator.py: - 2 importable-checks (function exists) - 3 callback-logic tests (timestamp, failures, thread-safety) - 1 live_gui smoke test (controller exposes warmup_status)
5644 lines
259 KiB
Python
5644 lines
259 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 defer import defer
|
|
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
|
|
|
|
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:
|
|
self._cached = getattr(mod, self._attr_name)
|
|
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)
|
|
|
|
# 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
|
|
|
|
if sys.platform == "win32":
|
|
import win32gui
|
|
import win32con
|
|
else:
|
|
win32gui = None
|
|
win32con = 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
|
|
|
|
class App:
|
|
"""The main ImGui interface orchestrator for Manual Slop."""
|
|
|
|
def __init__(self) -> None:
|
|
# Initialize controller and delegate state
|
|
"""
|
|
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
|
"""
|
|
# --- Core Dependencies & State ---
|
|
self.controller = app_controller.AppController()
|
|
self.controller._app = self
|
|
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 ---
|
|
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']
|
|
))
|
|
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()
|
|
self.controller.start_services(self)
|
|
# --- 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'
|
|
|
|
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,
|
|
})
|
|
# --- 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)
|
|
# --- 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] = {}
|
|
# --- 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 = []
|
|
# --- 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"):
|
|
try:
|
|
self.controller.on_warmup_complete(lambda status: _on_warmup_complete_callback(self, status))
|
|
except Exception: pass
|
|
|
|
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 and starts the main application loop.
|
|
[C: simulation/sim_base.py:run_sim, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
|
"""
|
|
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:
|
|
theme.load_from_config(self.config)
|
|
self.runner_params = hello_imgui.RunnerParams()
|
|
self.runner_params.app_window_params.window_title = "manual slop"
|
|
|
|
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)
|
|
fps_cap = 60.0
|
|
if sys.platform == "win32":
|
|
try:
|
|
# Use PowerShell to get max refresh rate across all controllers
|
|
cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\""
|
|
out = subprocess.check_output(cmd, shell=True).decode().splitlines()
|
|
rates = [float(r.strip()) for r in out if r.strip().isdigit()]
|
|
if rates: fps_cap = max(rates)
|
|
except Exception: pass
|
|
|
|
# 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"
|
|
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 = theme.apply_current
|
|
self.runner_params.callbacks.post_init = self._post_init
|
|
self._fetch_models(self.current_provider)
|
|
md_options = markdown_helper.get_renderer().options
|
|
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
|
|
# On exit
|
|
self.shutdown()
|
|
session_logger.close_session()
|
|
|
|
def _load_fonts(self) -> None:
|
|
# 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:
|
|
p = Path(font_path)
|
|
if p.is_absolute():
|
|
try:
|
|
if p.is_relative_to(assets_dir):
|
|
font_path = str(p.relative_to(assets_dir)).replace("\\", "/")
|
|
except (ValueError, AttributeError):
|
|
pass # Fallback to original font_path if relative_to fails or on old Python
|
|
|
|
# Just try loading it directly; hello_imgui will look in the assets folder
|
|
try:
|
|
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
|
|
except Exception as e:
|
|
print(f"Failed to load main font {font_path}: {e}")
|
|
self.main_font = None
|
|
else:
|
|
self.main_font = None
|
|
|
|
try:
|
|
params = hello_imgui.FontLoadingParams(font_config=config)
|
|
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
|
|
except Exception as e:
|
|
print(f"Failed to load mono font: {e}")
|
|
self.mono_font = None
|
|
|
|
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)
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
if name == 'controller':
|
|
raise AttributeError(name)
|
|
return getattr(self.controller, 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
|
|
|
|
@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:
|
|
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:
|
|
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
|
|
|
|
def _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:
|
|
if not getattr(self, "_ini_capture_ready", False):
|
|
self._ini_capture_ready = True
|
|
ini = ""
|
|
else:
|
|
try:
|
|
ini = str(imgui.save_ini_settings_to_memory() or "")
|
|
except Exception:
|
|
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):
|
|
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:
|
|
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:
|
|
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 and saves state.
|
|
[C: tests/conftest.py:app_instance, tests/conftest.py:mock_app]
|
|
"""
|
|
try:
|
|
if hasattr(self, 'runner_params') and self.runner_params.ini_filename:
|
|
imgui.save_ini_settings_to_disk(self.runner_params.ini_filename)
|
|
except:
|
|
pass
|
|
self.controller.shutdown()
|
|
|
|
def load_context_preset(self, name: str) -> None:
|
|
"""
|
|
[C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset]
|
|
"""
|
|
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:
|
|
"""
|
|
[C: tests/test_context_presets.py:test_delete_context_preset, tests/test_context_presets.py:test_delete_nonexistent_preset_no_error]
|
|
"""
|
|
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)
|
|
# ---------------------------------------------------------------- helpers
|
|
|
|
# ---------------------------------------------------------------- gui
|
|
|
|
def _gui_func(self) -> None:
|
|
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")
|
|
try:
|
|
if self.is_viewing_prior_session:
|
|
with imscope.style_color(imgui.Col_.window_bg, theme.get_color("bubble_vendor")):
|
|
render_main_interface(self)
|
|
else:
|
|
render_main_interface(self)
|
|
except Exception as e:
|
|
sys.stderr.write(f"ERROR in _gui_func: {e}\n")
|
|
traceback.print_exc()
|
|
|
|
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 _show_menus(self) -> None:
|
|
"""
|
|
[C: tests/test_gui_window_controls.py:test_gui_window_controls_minimize_maximize_close]
|
|
"""
|
|
|
|
with imscope.menu("manual slop") as (active):
|
|
if active and imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
|
|
self.runner_params.app_shall_exit = True
|
|
with imscope.menu("Windows") as (active):
|
|
if (active):
|
|
for w in self.show_windows.keys():
|
|
_, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w])
|
|
with imscope.menu("Project") as (active):
|
|
if active:
|
|
if imgui.menu_item("Save All", "", False)[0]:
|
|
self._flush_to_project()
|
|
self._flush_to_config()
|
|
models.save_config(self.config)
|
|
self.ai_status = "config saved"
|
|
if imgui.menu_item("Reset Session", "", False)[0]:
|
|
ai_client.reset_session()
|
|
ai_client.clear_comms_log()
|
|
self._tool_log.clear()
|
|
self._comms_log.clear()
|
|
self.ai_status = "session reset"
|
|
self.ai_response = ""
|
|
if imgui.menu_item("Generate MD Only", "", False)[0]:
|
|
try:
|
|
md, path, *_ = self._do_generate()
|
|
self.last_md = md
|
|
self.last_md_path = path
|
|
self.ai_status = f"md written: {path.name}"
|
|
except Exception as e:
|
|
self.ai_status = f"error: {e}"
|
|
with imscope.menu("Layout") as (active):
|
|
if active:
|
|
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":
|
|
try:
|
|
import ctypes
|
|
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
|
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
|
|
hwnd_capsule = imgui.get_main_viewport().platform_handle_raw
|
|
hwnd = ctypes.pythonapi.PyCapsule_GetPointer(hwnd_capsule, b"nb_handle")
|
|
except Exception:
|
|
hwnd = 0
|
|
|
|
if hwnd:
|
|
btn_w = 40
|
|
# Use window width (points) instead of display_size (pixels) for correct scaling
|
|
window_w = imgui.get_window_width()
|
|
bar_h = imgui.get_window_height()
|
|
right_x = window_w - (btn_w * 3)
|
|
# Drag area check using an explicit invisible button spanning the empty space
|
|
curr_x = imgui.get_cursor_pos_x()
|
|
drag_w = right_x - curr_x
|
|
if drag_w > 0:
|
|
imgui.invisible_button("##drag_area", (drag_w, bar_h))
|
|
if imgui.is_item_active() and imgui.is_mouse_dragging(0):
|
|
# CRITICAL: We must reset ImGui's mouse_down state BEFORE passing control to Windows.
|
|
# Otherwise, the Windows modal drag loop swallows the WM_LBUTTONUP event,
|
|
# and ImGui thinks the mouse is permanently held down, causing "sticky" dragging.
|
|
imgui.get_io().mouse_down[0] = False
|
|
win32gui.ReleaseCapture()
|
|
win32gui.SendMessage(hwnd, win32con.WM_NCLBUTTONDOWN, win32con.HTCAPTION, 0)
|
|
|
|
imgui.push_style_color(imgui.Col_.button, imgui.ImVec4(0, 0, 0, 0))
|
|
|
|
try:
|
|
is_max = win32gui.GetWindowPlacement(hwnd)[1] == win32con.SW_SHOWMAXIMIZED
|
|
except Exception:
|
|
is_max = False
|
|
|
|
# Explicitly set Y to 0 and match button height to bar height for perfect alignment
|
|
imgui.set_cursor_pos((right_x, 0))
|
|
if imgui.button("_", (btn_w, bar_h)):
|
|
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
|
|
|
|
imgui.set_cursor_pos((right_x + btn_w, 0))
|
|
if imgui.button("[=]" if is_max else "[]", (btn_w, bar_h)):
|
|
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE if is_max else win32con.SW_MAXIMIZE)
|
|
|
|
imgui.set_cursor_pos((right_x + btn_w * 2, 0))
|
|
imgui.push_style_color(imgui.Col_.button_hovered, 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
|
|
|
|
try:
|
|
# 2. Debounced snapshotting
|
|
current = self._take_snapshot()
|
|
if self._last_ui_snapshot is None:
|
|
self._last_ui_snapshot = current
|
|
return
|
|
|
|
# Compare only core fields for performance
|
|
changed = (
|
|
current.ai_input != self._last_ui_snapshot.ai_input or
|
|
current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or
|
|
current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or
|
|
current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or
|
|
current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or
|
|
abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or
|
|
abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or
|
|
current.max_tokens != self._last_ui_snapshot.max_tokens or
|
|
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
|
|
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or
|
|
len(current.files) != len(self._last_ui_snapshot.files) or
|
|
len(current.context_files) != len(self._last_ui_snapshot.context_files) or
|
|
len(current.screenshots) != len(self._last_ui_snapshot.screenshots)
|
|
)
|
|
|
|
if not changed and len(current.disc_entries) > 0:
|
|
if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'):
|
|
changed = True
|
|
|
|
if changed:
|
|
if not self._pending_snapshot:
|
|
self._pending_snapshot = True
|
|
self._snapshot_timer = time.time()
|
|
# Capture state BEFORE current change
|
|
self._state_to_push = self._last_ui_snapshot
|
|
else:
|
|
# Reset timer for settle debounce
|
|
self._snapshot_timer = time.time()
|
|
|
|
self._last_ui_snapshot = current
|
|
|
|
if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce):
|
|
if self._state_to_push:
|
|
self.history.push(self._state_to_push, "UI Update")
|
|
self._state_to_push = None
|
|
self._pending_snapshot = False
|
|
except Exception as e:
|
|
import sys, traceback
|
|
sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n")
|
|
traceback.print_exc(file=sys.stderr)
|
|
sys.stderr.flush()
|
|
|
|
def 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
|
|
models.save_config(self.config)
|
|
self.ai_status = f"Default editor set to: {editor_name}"
|
|
|
|
_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)): self._save_paths()
|
|
imgui.same_line()
|
|
if imgui.button("Reset", imgui.ImVec2(120, 0)):
|
|
self.init_state()
|
|
self.ai_status = "paths reset to defaults"
|
|
|
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
|
|
|
def _save_paths(self) -> None:
|
|
"""
|
|
[C: tests/test_gui_paths.py:test_save_paths]
|
|
"""
|
|
self.config["paths"] = {
|
|
"logs_dir": self.ui_logs_dir,
|
|
"scripts_dir": self.ui_scripts_dir
|
|
}
|
|
cfg_path = paths.get_config_path()
|
|
if cfg_path.exists(): shutil.copy(cfg_path, str(cfg_path) + ".bak")
|
|
models.save_config(self.config)
|
|
paths.reset_resolved()
|
|
self.init_state()
|
|
self.ai_status = 'paths applied and session reset'
|
|
|
|
def _populate_auto_slices(self, f_item: models.FileItem) -> None:
|
|
"""
|
|
[C: tests/test_auto_slices.py:test_populate_auto_slices_basic]
|
|
"""
|
|
from src import mcp_client
|
|
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)
|
|
|
|
f_path_lower = f_item.path.lower()
|
|
try:
|
|
if f_path_lower.endswith('.py'): outline = mcp_client.py_get_code_outline(abs_path)
|
|
elif f_path_lower.endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(abs_path)
|
|
elif f_path_lower.endswith(('.cpp', '.hpp', '.cxx', '.cc')): outline = mcp_client.ts_cpp_get_code_outline(abs_path)
|
|
else: return
|
|
except Exception:
|
|
return
|
|
|
|
if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"):
|
|
return
|
|
pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE)
|
|
try:
|
|
with open(f_item.path, "r", encoding="utf-8") as f:
|
|
text = f.read()
|
|
except Exception:
|
|
return
|
|
try:
|
|
from src.fuzzy_anchor import FuzzyAnchor
|
|
except ImportError:
|
|
FuzzyAnchor = None
|
|
for match in pattern.finditer(outline):
|
|
kind, name, s_str, e_str = match.groups()
|
|
s_line = int(s_str)
|
|
e_line = int(e_str)
|
|
if any(s.get('start_line') == s_line and s.get('end_line') == e_line for s in f_item.custom_slices):
|
|
continue
|
|
if FuzzyAnchor:
|
|
slice_data = FuzzyAnchor.create_slice(text, s_line, e_line)
|
|
else:
|
|
slice_data = {"start_line": s_line, "end_line": e_line}
|
|
slice_data['tag'] = 'auto-ast'
|
|
slice_data['comment'] = name
|
|
f_item.custom_slices.append(slice_data)
|
|
|
|
def _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:
|
|
stats = self._file_stats_cache[cache_key]
|
|
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:
|
|
try:
|
|
self._vscode_diff_process.terminate()
|
|
except Exception:
|
|
pass
|
|
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
|
|
try:
|
|
base_dir = str(self.controller.current_project_dir) if hasattr(self.controller, 'current_project_dir') else "."
|
|
success, msg = apply_patch_to_file(self._pending_patch_text, base_dir)
|
|
if success:
|
|
self._show_patch_modal = False
|
|
self._pending_patch_text = None
|
|
self._pending_patch_files = []
|
|
self._patch_error_message = None
|
|
imgui.close_current_popup()
|
|
else:
|
|
self._patch_error_message = msg
|
|
except Exception as e:
|
|
self._patch_error_message = str(e)
|
|
|
|
def _open_patch_in_external_editor(self) -> None:
|
|
self._external_editor_clicked = True
|
|
try:
|
|
from src.external_editor import get_default_launcher, create_temp_modified_file
|
|
import os
|
|
if not self._pending_patch_files:
|
|
self._patch_error_message = "No files to edit"
|
|
return
|
|
launcher = get_default_launcher()
|
|
editor = launcher.config.get_default()
|
|
if not editor:
|
|
self._patch_error_message = "No external editor configured"
|
|
return
|
|
original_path = self._pending_patch_files[0]
|
|
if not os.path.exists(original_path):
|
|
self._patch_error_message = f"Original file not found: {original_path}"
|
|
return
|
|
temp_path = create_temp_modified_file(self._pending_patch_text)
|
|
result = launcher.launch_diff(None, original_path, temp_path)
|
|
if result is None: self._patch_error_message = "Failed to launch external editor"
|
|
else:
|
|
self._patch_error_message = None
|
|
self._vscode_diff_process = result
|
|
except Exception as e:
|
|
self._patch_error_message = str(e)
|
|
|
|
def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None:
|
|
"""
|
|
[C: tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_invalid, tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_valid]
|
|
"""
|
|
if src_idx == dst_idx: return
|
|
new_tickets = list(self.active_tickets)
|
|
ticket = new_tickets.pop(src_idx)
|
|
new_tickets.insert(dst_idx, ticket)
|
|
# Validate dependencies: a ticket cannot be placed before any of its dependencies
|
|
id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)}
|
|
valid = True
|
|
for i, t in enumerate(new_tickets):
|
|
deps = t.get('depends_on', [])
|
|
for d_id in deps:
|
|
if d_id in id_to_idx and id_to_idx[d_id] >= i:
|
|
valid = False
|
|
break
|
|
if not valid: break
|
|
if valid:
|
|
self.active_tickets = new_tickets
|
|
self._push_mma_state_update()
|
|
|
|
def request_patch_from_tier4(self, error: str, file_context: str) -> None:
|
|
try:
|
|
from src import ai_client
|
|
from src.diff_viewer import parse_diff
|
|
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]
|
|
self._pending_patch_text = patch_text
|
|
self._pending_patch_files = file_paths
|
|
self._show_patch_modal = True
|
|
else:
|
|
self._patch_error_message = patch_text or "No patch generated"
|
|
except Exception as e:
|
|
self._patch_error_message = str(e)
|
|
|
|
def bulk_execute(self) -> None:
|
|
"""
|
|
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_execute]
|
|
"""
|
|
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:
|
|
"""
|
|
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_skip]
|
|
"""
|
|
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:
|
|
"""
|
|
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_block]
|
|
"""
|
|
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 main() -> None:
|
|
app = App()
|
|
app.run()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
def render_main_interface(app: App) -> None:
|
|
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
|
|
#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)
|
|
|
|
# Auto-save (every 60s)
|
|
now = time.time()
|
|
if now - app._last_autosave >= app._autosave_interval:
|
|
app._last_autosave = now
|
|
try:
|
|
app._flush_to_project()
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
except Exception:
|
|
pass # silent — don't disrupt the GUI loop
|
|
|
|
# Sync pending comms
|
|
with app._pending_comms_lock:
|
|
if app._pending_comms:
|
|
if app.ui_auto_scroll_comms: app._scroll_comms_to_bottom = True
|
|
app._comms_log_dirty = True
|
|
for c in app._pending_comms: app._comms_log.append(c)
|
|
app._pending_comms.clear()
|
|
|
|
if app.ui_focus_agent != app._last_ui_focus_agent:
|
|
app._comms_log_dirty = True
|
|
app._tool_log_dirty = True
|
|
app._last_ui_focus_agent = app.ui_focus_agent
|
|
|
|
if app._comms_log_dirty:
|
|
if app.is_viewing_prior_session: app._comms_log_cache = app.prior_session_entries
|
|
else:
|
|
log_raw = list(app._comms_log)
|
|
if app.ui_focus_agent: app._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)]
|
|
else: app._comms_log_cache = log_raw
|
|
app._comms_log_dirty = False
|
|
|
|
if app._tool_log_dirty:
|
|
if app.is_viewing_prior_session: app._tool_log_cache = app.prior_tool_calls
|
|
else:
|
|
log_raw = list(app._tool_log)
|
|
if app.ui_focus_agent: app._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)]
|
|
else: app._tool_log_cache = log_raw
|
|
app._tool_log_dirty = False
|
|
|
|
app._render_window_if_open("Project Settings", 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", 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())
|
|
render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", 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()
|
|
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:
|
|
"""
|
|
[C: tests/test_log_management_ui.py:test_render_log_management_logic]
|
|
"""
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_log_management")
|
|
with imscope.window("Log Management", app.show_windows["Log Management"]) as (exp, opened):
|
|
app.show_windows["Log Management"] = bool(opened)
|
|
|
|
if 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")
|
|
metadata = s_data.get("metadata") or {}
|
|
imgui.table_next_column()
|
|
imgui.text(metadata.get("reason", ""))
|
|
imgui.table_next_column()
|
|
imgui.text(str(metadata.get("size_kb", "")))
|
|
imgui.table_next_column()
|
|
imgui.text(str(metadata.get("message_count", "")))
|
|
imgui.table_next_column()
|
|
if imgui.button(f"Load##{session_id}"):
|
|
app.cb_load_prior_log(s_data.get("path"))
|
|
imgui.same_line()
|
|
if whitelisted:
|
|
if imgui.button(f"Unstar##{session_id}"):
|
|
registry.update_session_metadata(
|
|
session_id,
|
|
message_count=int(metadata.get("message_count") or 0),
|
|
errors=int(metadata.get("errors") or 0),
|
|
size_kb=int(metadata.get("size_kb") or 0),
|
|
whitelisted=False,
|
|
reason=str(metadata.get("reason") or "")
|
|
)
|
|
else:
|
|
if imgui.button(f"Star##{session_id}"):
|
|
registry.update_session_metadata(
|
|
session_id,
|
|
message_count=int(metadata.get("message_count") or 0),
|
|
errors=int(metadata.get("errors") or 0),
|
|
size_kb=int(metadata.get("size_kb") or 0),
|
|
whitelisted=True,
|
|
reason="Manually whitelisted"
|
|
)
|
|
imgui.end_table()
|
|
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management")
|
|
|
|
#endregion: Logging
|
|
|
|
#region: Project Management
|
|
|
|
def render_project_settings_hub(app: App) -> None:
|
|
with imscope.tab_bar('context_hub_tabs'):
|
|
with imscope.tab_item('Projects') as (exp, _):
|
|
if exp: render_projects_panel(app)
|
|
with imscope.tab_item('Paths') as (exp, _):
|
|
if exp: render_paths_panel(app)
|
|
|
|
def render_projects_panel(app: App) -> None:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_projects_panel")
|
|
proj_name = app.project.get("project", {}).get("name", Path(app.active_project_path).stem)
|
|
imgui.text_colored(C_IN(), f"Active: {proj_name}")
|
|
imgui.separator()
|
|
imgui.text("Execution Mode")
|
|
modes = ["native", "beads"]
|
|
current_idx = modes.index(app.ui_project_execution_mode) if app.ui_project_execution_mode in modes else 0
|
|
ch, new_idx = imgui.combo("##exec_mode", current_idx, modes)
|
|
if ch: app.ui_project_execution_mode = modes[new_idx]
|
|
imgui.separator()
|
|
imgui.text("Git Directory")
|
|
ch, app.ui_project_git_dir = imgui.input_text("##git_dir", app.ui_project_git_dir)
|
|
imgui.same_line()
|
|
if imgui.button("Browse##git"):
|
|
r = hide_tk_root()
|
|
d = filedialog.askdirectory(title="Select Git Directory")
|
|
r.destroy()
|
|
if d: app.ui_project_git_dir = d
|
|
imgui.separator()
|
|
imgui.text("Output Dir")
|
|
ch, app.ui_output_dir = imgui.input_text("##out_dir", app.ui_output_dir)
|
|
imgui.same_line()
|
|
if imgui.button("Browse##out"):
|
|
r = hide_tk_root()
|
|
d = filedialog.askdirectory(title="Select Output Dir")
|
|
r.destroy()
|
|
if d: app.ui_output_dir = d
|
|
imgui.separator()
|
|
imgui.text("Conductor Directory")
|
|
ch, app.ui_project_conductor_dir = imgui.input_text("##cond_dir", app.ui_project_conductor_dir)
|
|
imgui.same_line()
|
|
if imgui.button("Browse##cond"):
|
|
r = hide_tk_root()
|
|
d = filedialog.askdirectory(title="Select Conductor Directory")
|
|
r.destroy()
|
|
if d: app.ui_project_conductor_dir = d
|
|
imgui.separator()
|
|
imgui.text("Project Files")
|
|
imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True)
|
|
for i, pp in enumerate(app.project_paths):
|
|
is_active = (pp == app.active_project_path)
|
|
if imgui.button(f"x##p{i}"):
|
|
removed = app.project_paths.pop(i)
|
|
if removed == app.active_project_path and app.project_paths: app._switch_project(app.project_paths[0])
|
|
break
|
|
imgui.same_line()
|
|
marker = " *" if is_active else ""
|
|
if is_active: imgui.push_style_color(imgui.Col_.text, C_IN())
|
|
if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): app._switch_project(pp)
|
|
if is_active: imgui.pop_style_color()
|
|
imgui.same_line()
|
|
imgui.text_colored(C_LBL(), pp)
|
|
imgui.end_child()
|
|
if imgui.button("Add Project"):
|
|
r = hide_tk_root()
|
|
p = filedialog.askopenfilename(
|
|
title="Select Project .toml",
|
|
filetypes=[("TOML", "*.toml"), ("All", "*.*")],
|
|
)
|
|
r.destroy()
|
|
if p and p not in app.project_paths:
|
|
app.project_paths.append(p)
|
|
imgui.same_line()
|
|
if imgui.button("New Project"):
|
|
r = hide_tk_root()
|
|
p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")])
|
|
r.destroy()
|
|
if p:
|
|
name = Path(p).stem
|
|
proj = project_manager.default_project(name)
|
|
project_manager.save_project(proj, p)
|
|
if p not in app.project_paths: app.project_paths.append(p)
|
|
app._switch_project(p)
|
|
imgui.same_line()
|
|
if imgui.button("Save All"):
|
|
app._flush_to_project()
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
app.ai_status = "config saved"
|
|
ch, app.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", app.ui_word_wrap)
|
|
ch, app.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", app.ui_auto_scroll_comms)
|
|
ch, app.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", app.ui_auto_scroll_tool_calls)
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_projects_panel")
|
|
|
|
def render_paths_panel(app: App) -> None:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_paths_panel")
|
|
path_info = paths.get_full_path_info()
|
|
|
|
imgui.text_colored(C_IN(), "System Path Configuration")
|
|
imgui.separator()
|
|
|
|
def render_path_field(label: str, attr: str, key: str, tooltip: str):
|
|
info = path_info.get(key, {'source': 'unknown'})
|
|
imgui.text(label)
|
|
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
|
|
imgui.same_line()
|
|
imgui.text_disabled(f"(Source: {info['source']})")
|
|
|
|
val = getattr(app, attr)
|
|
changed, new_val = imgui.input_text(f"##{key}", val)
|
|
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
|
|
if changed: setattr(app, attr, new_val)
|
|
imgui.same_line()
|
|
if imgui.button(f"Browse##{key}"):
|
|
r = hide_tk_root()
|
|
d = filedialog.askdirectory(title=f"Select {label}")
|
|
r.destroy()
|
|
if d: setattr(app, attr, d)
|
|
|
|
#endregion: Project Management
|
|
|
|
#region: AI Settings
|
|
|
|
def render_ai_settings_hub(app: App) -> None:
|
|
render_persona_selector_panel(app)
|
|
if imgui.collapsing_header("Provider & Model"): render_provider_panel(app)
|
|
if imgui.collapsing_header("System Prompts"): render_system_prompts_panel(app)
|
|
if imgui.collapsing_header("RAG Settings"): render_rag_panel(app)
|
|
render_agent_tools_panel(app)
|
|
|
|
def render_rag_panel(app: App) -> None:
|
|
conf = app.controller.rag_config
|
|
if not conf: return
|
|
ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled)
|
|
|
|
imgui.text("Vector Store Provider")
|
|
providers = ['chroma', 'qdrant', 'mock']
|
|
try:
|
|
idx = providers.index(conf.vector_store.provider)
|
|
except (ValueError, AttributeError):
|
|
idx = 0
|
|
ch2, next_idx = imgui.combo("##rag_provider", idx, providers)
|
|
if ch2:
|
|
conf.vector_store.provider = providers[next_idx]
|
|
|
|
imgui.text("Embedding Provider")
|
|
emb_providers = ['gemini', 'local']
|
|
try:
|
|
idx_e = emb_providers.index(conf.embedding_provider)
|
|
except (ValueError, AttributeError):
|
|
idx_e = 0
|
|
ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers)
|
|
if ch3:
|
|
conf.embedding_provider = emb_providers[next_idx_e]
|
|
|
|
imgui.text("Chunk Size")
|
|
imgui.set_next_item_width(150)
|
|
ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size)
|
|
imgui.text("Chunk Overlap")
|
|
imgui.set_next_item_width(150)
|
|
ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap)
|
|
|
|
imgui.separator()
|
|
imgui.text(f"Status: {app.controller.rag_status}")
|
|
|
|
if imgui.button("Rebuild Index"): app.controller.event_queue.put('click', 'btn_rebuild_rag_index')
|
|
|
|
def render_system_prompts_panel(app: App) -> None:
|
|
imgui.text("Global System Prompt (all projects)")
|
|
preset_names = sorted(app.controller.presets.keys())
|
|
current_global = app.controller.ui_global_preset_name or "Select Preset..."
|
|
imgui.set_next_item_width(200)
|
|
if imgui.begin_combo("##global_preset", current_global):
|
|
for name in preset_names:
|
|
is_sel = (name == current_global)
|
|
if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "global")
|
|
if is_sel: imgui.set_item_default_focus()
|
|
imgui.end_combo()
|
|
imgui.same_line(0, 8)
|
|
if imgui.button("Manage Presets##global"): app.show_preset_manager_window = True
|
|
imgui.set_item_tooltip("Open preset management modal")
|
|
ch, app.ui_global_system_prompt = imgui.input_text_multiline("##gsp", app.ui_global_system_prompt, imgui.ImVec2(-1, 100))
|
|
imgui.separator()
|
|
_, app.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", app.ui_use_default_base_prompt)
|
|
imgui.same_line()
|
|
if imgui.button("Reset to Default##btn_reset_base_prompt"): app.controller._cb_reset_base_prompt()
|
|
imgui.same_line()
|
|
if imgui.button("Show Diff##btn_show_base_prompt_diff"): app.controller._cb_show_base_prompt_diff()
|
|
imgui.set_item_tooltip("Compare current base prompt with the default.")
|
|
|
|
imgui.same_line()
|
|
imgui.text_disabled("(?)")
|
|
imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.")
|
|
|
|
header_flags = imgui.TreeNodeFlags_.default_open if not app.ui_use_default_base_prompt else 0
|
|
if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags):
|
|
if app.ui_use_default_base_prompt:
|
|
imgui.begin_disabled()
|
|
imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only)
|
|
imgui.end_disabled()
|
|
imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}")
|
|
else:
|
|
ch, app.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", app.ui_base_system_prompt, imgui.ImVec2(-1, 150))
|
|
imgui.text_disabled(f"Characters: {len(app.ui_base_system_prompt)}")
|
|
imgui.separator()
|
|
imgui.text("Project System Prompt")
|
|
current_project = app.controller.ui_project_preset_name or "Select Preset..."
|
|
imgui.set_next_item_width(200)
|
|
if imgui.begin_combo("##project_preset", current_project):
|
|
for name in preset_names:
|
|
is_sel = (name == current_project)
|
|
if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "project")
|
|
if is_sel: imgui.set_item_default_focus()
|
|
imgui.end_combo()
|
|
imgui.same_line(0, 8)
|
|
if imgui.button("Manage Presets##project"): app.show_preset_manager_window = True
|
|
imgui.set_item_tooltip("Open preset management modal")
|
|
ch, app.ui_project_system_prompt = imgui.input_text_multiline("##psp", app.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
|
|
|
def render_agent_tools_panel(app: App) -> None:
|
|
if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open):
|
|
imgui.text("Tool Preset")
|
|
presets = app.controller.tool_presets
|
|
preset_names = [""] + sorted(list(presets.keys()))
|
|
|
|
active = getattr(app, "ui_active_tool_preset", "")
|
|
if active is None: active = ""
|
|
try:
|
|
idx = preset_names.index(active)
|
|
except ValueError:
|
|
idx = 0
|
|
|
|
ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names)
|
|
if ch:
|
|
app.ui_active_tool_preset = preset_names[new_idx]
|
|
|
|
imgui.same_line()
|
|
if imgui.button("Manage Presets##tools"): app.show_tool_preset_manager_window = True
|
|
if imgui.is_item_hovered(): imgui.set_tooltip("Configure tool availability and default modes.")
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 4))
|
|
imgui.text("Bias Profile")
|
|
if imgui.begin_combo("##bias", getattr(app, 'ui_active_bias_profile', "") or "None"):
|
|
if imgui.selectable("None", not getattr(app, 'ui_active_bias_profile', ""))[0]:
|
|
app.ui_active_bias_profile = ""
|
|
ai_client.set_bias_profile(None)
|
|
for bname in sorted(app.controller.bias_profiles.keys()):
|
|
if not bname: continue
|
|
if imgui.selectable(bname, bname == getattr(app, 'ui_active_bias_profile', ""))[0]:
|
|
app.ui_active_bias_profile = bname
|
|
ai_client.set_bias_profile(bname)
|
|
imgui.end_combo()
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 8))
|
|
cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys()))
|
|
try:
|
|
f_idx = cat_options.index(app.ui_tool_filter_category)
|
|
except ValueError:
|
|
f_idx = 0
|
|
imgui.set_next_item_width(200)
|
|
ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options)
|
|
if ch_cat: app.ui_tool_filter_category = cat_options[next_f_idx]
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 8))
|
|
active_name = app.ui_active_tool_preset
|
|
if active_name and active_name in presets:
|
|
preset = presets[active_name]
|
|
for cat_name, tools in preset.categories.items():
|
|
if app.ui_tool_filter_category != "All" and app.ui_tool_filter_category != cat_name: continue
|
|
if imgui.tree_node(cat_name):
|
|
for tool in tools:
|
|
if tool.weight >= 5: imgui.text_colored(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:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_provider_panel")
|
|
imgui.text("Provider")
|
|
if imgui.begin_combo("##prov", app.current_provider):
|
|
for p in models.PROVIDERS:
|
|
if imgui.selectable(p, p == app.current_provider)[0]:
|
|
app.current_provider = p
|
|
imgui.end_combo()
|
|
imgui.separator()
|
|
imgui.text("Model")
|
|
if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)):
|
|
for m in app.available_models:
|
|
if imgui.selectable(m, m == app.current_model)[0]:
|
|
app.current_model = m
|
|
imgui.end_list_box()
|
|
imgui.separator()
|
|
imgui.text("Parameters")
|
|
# Temperature
|
|
imgui.push_id("temp")
|
|
imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6)
|
|
_, app.temperature = imgui.slider_float("##slider", app.temperature, 0.0, 2.0, "%.2f")
|
|
imgui.same_line()
|
|
imgui.set_next_item_width(-1)
|
|
_, app.temperature = imgui.input_float("Temp", app.temperature, 0.0, 0.0, "%.2f")
|
|
imgui.pop_id()
|
|
|
|
# Top-P
|
|
imgui.push_id("top_p")
|
|
imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6)
|
|
_, app.top_p = imgui.slider_float("##slider", app.top_p, 0.0, 1.0, "%.2f")
|
|
imgui.same_line()
|
|
imgui.set_next_item_width(-1)
|
|
_, app.top_p = imgui.input_float("Top-P", app.top_p, 0.0, 0.0, "%.2f")
|
|
imgui.pop_id()
|
|
|
|
# Max Tokens
|
|
imgui.push_id("max_tokens")
|
|
imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6)
|
|
_, app.max_tokens = imgui.slider_int("##slider", app.max_tokens, 1, 32768)
|
|
imgui.same_line()
|
|
imgui.set_next_item_width(-1)
|
|
_, app.max_tokens = imgui.input_int("MaxTok", app.max_tokens)
|
|
imgui.pop_id()
|
|
|
|
ch, app.history_trunc_limit = imgui.input_int("History Truncation Limit", app.history_trunc_limit, 1024)
|
|
|
|
if app.current_provider == "gemini_cli":
|
|
imgui.separator()
|
|
imgui.text("Gemini CLI")
|
|
sid = "None"
|
|
if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None"
|
|
imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200)
|
|
if imgui.button("Reset CLI Session"): ai_client.reset_session()
|
|
imgui.text("Binary Path")
|
|
ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path)
|
|
imgui.same_line()
|
|
if imgui.button("Browse##gcli"):
|
|
r = hide_tk_root()
|
|
p = filedialog.askopenfilename(title="Select gemini CLI binary")
|
|
r.destroy()
|
|
if p: app.ui_gemini_cli_path = p
|
|
if ch:
|
|
if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter:
|
|
ai_client._gemini_cli_adapter.binary_path = app.ui_gemini_cli_path
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_provider_panel")
|
|
|
|
def render_persona_selector_panel(app: App) -> None:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_persona_selector_panel")
|
|
imgui.text("Persona")
|
|
if not hasattr(app, 'ui_active_persona'): app.ui_active_persona = ""
|
|
personas = getattr(app.controller, 'personas', {})
|
|
if imgui.begin_combo("##persona", app.ui_active_persona or "None"):
|
|
if imgui.selectable("None", not app.ui_active_persona)[0]: app.ui_active_persona = ""
|
|
for pname in sorted(personas.keys()):
|
|
if not pname: continue
|
|
if imgui.selectable(pname, pname == app.ui_active_persona)[0]:
|
|
app.ui_active_persona = pname
|
|
if pname in personas:
|
|
persona = personas[pname]
|
|
app._editing_persona_name = persona.name
|
|
app._editing_persona_system_prompt = persona.system_prompt or ""
|
|
app._editing_persona_tool_preset_id = persona.tool_preset or ""
|
|
app._editing_persona_bias_profile_id = persona.bias_profile or ""
|
|
app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or ""
|
|
app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or ""
|
|
app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
|
app._editing_persona_is_new = False
|
|
|
|
# Apply persona to current state immediately
|
|
if persona.preferred_models and len(persona.preferred_models) > 0:
|
|
first_model = persona.preferred_models[0]
|
|
if first_model.get("provider"):
|
|
app.current_provider = first_model.get("provider")
|
|
if first_model.get("model"):
|
|
app.current_model = first_model.get("model")
|
|
if first_model.get("temperature") is not None:
|
|
ai_client.temperature = first_model.get("temperature")
|
|
app.temperature = first_model.get("temperature")
|
|
if first_model.get("max_output_tokens"):
|
|
ai_client.max_output_tokens = first_model.get("max_output_tokens")
|
|
app.max_tokens = first_model.get("max_output_tokens")
|
|
if first_model.get("history_trunc_limit"):
|
|
app.history_trunc_limit = first_model.get("history_trunc_limit")
|
|
|
|
if persona.system_prompt: app.ui_project_system_prompt = persona.system_prompt
|
|
if persona.tool_preset:
|
|
app.ui_active_tool_preset = persona.tool_preset
|
|
ai_client.set_tool_preset(persona.tool_preset)
|
|
if persona.bias_profile:
|
|
app.ui_active_bias_profile = persona.bias_profile
|
|
ai_client.set_bias_profile(persona.bias_profile)
|
|
if getattr(persona, 'context_preset', None):
|
|
app.ui_active_context_preset = persona.context_preset
|
|
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:
|
|
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:
|
|
if not app._show_save_preset_modal: return
|
|
imgui.open_popup("Save Layout Preset")
|
|
with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _):
|
|
if opened:
|
|
imgui.text("Preset Name:")
|
|
_, app._new_preset_name = imgui.input_text("##preset_name", app._new_preset_name)
|
|
if imgui.button("Save", imgui.ImVec2(120, 0)):
|
|
if app._new_preset_name.strip():
|
|
ini_data = imgui.save_ini_settings_to_memory()
|
|
app.layout_presets[app._new_preset_name.strip()] = {
|
|
"ini": ini_data,
|
|
"multi_viewport": app.ui_multi_viewport
|
|
}
|
|
app.config["layout_presets"] = app.layout_presets
|
|
models.save_config(app.config)
|
|
app._show_save_preset_modal = False
|
|
app._new_preset_name = ""
|
|
imgui.close_current_popup()
|
|
imgui.same_line()
|
|
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
|
app._show_save_preset_modal = False
|
|
imgui.close_current_popup()
|
|
|
|
def render_preset_manager_content(app: App, is_embedded: bool = False) -> None:
|
|
avail = imgui.get_content_region_avail()
|
|
if not hasattr(app, "_prompt_md_preview"): app._prompt_md_preview = False
|
|
|
|
if imgui.begin_table("prompt_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v):
|
|
imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200)
|
|
imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row()
|
|
|
|
# Left Sidebar
|
|
imgui.table_next_column()
|
|
imgui.begin_child("prompt_list_pane", imgui.ImVec2(0, 0), False)
|
|
if True:
|
|
if imgui.button("New Preset", imgui.ImVec2(-1, 0)):
|
|
app._editing_preset_name = ""
|
|
app._editing_preset_system_prompt = ""
|
|
app._editing_preset_scope = "project"
|
|
app._selected_preset_idx = -1
|
|
imgui.separator()
|
|
preset_names = sorted(app.controller.presets.keys())
|
|
for i, name in enumerate(preset_names):
|
|
if name and imgui.selectable(f"{name}##p_{i}", app._selected_preset_idx == i)[0]:
|
|
app._selected_preset_idx = i
|
|
app._editing_preset_name = name
|
|
p = app.controller.presets[name]
|
|
app._editing_preset_system_prompt = p.system_prompt
|
|
app._editing_preset_scope = app.controller.preset_manager.get_preset_scope(name)
|
|
imgui.end_child()
|
|
|
|
# Right Editor
|
|
imgui.table_next_column()
|
|
avail_r = imgui.get_content_region_avail()
|
|
imgui.begin_child("prompt_edit_pane", imgui.ImVec2(0, avail_r.y - 45), False)
|
|
if True:
|
|
p_disp = app._editing_preset_name or "(New Preset)"
|
|
imgui.text_colored(C_IN(), f"Editing Prompt Preset: {p_disp}")
|
|
imgui.separator()
|
|
|
|
if imgui.begin_table("p_meta", 2):
|
|
imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80)
|
|
imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row()
|
|
imgui.table_next_column(); imgui.text("Name:")
|
|
imgui.table_next_column(); imgui.set_next_item_width(-1)
|
|
_, app._editing_preset_name = imgui.input_text("##epn", app._editing_preset_name)
|
|
imgui.table_next_row()
|
|
imgui.table_next_column(); imgui.text("Scope:")
|
|
imgui.table_next_column()
|
|
if imgui.radio_button("Global##ps", app._editing_preset_scope == "global"): app._editing_preset_scope = "global"
|
|
imgui.same_line()
|
|
if imgui.radio_button("Project##ps", app._editing_preset_scope == "project"): app._editing_preset_scope = "project"
|
|
imgui.end_table()
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 4))
|
|
imgui.separator()
|
|
imgui.text("Prompt Content:")
|
|
imgui.same_line()
|
|
if imgui.button("Pop out MD Preview"):
|
|
app.text_viewer_title = f"Preset: {app._editing_preset_name}"
|
|
app.text_viewer_content = app._editing_preset_system_prompt
|
|
app.text_viewer_type = "markdown"
|
|
app.show_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:
|
|
if not app.show_preset_manager_window and not is_embedded: return
|
|
if not is_embedded:
|
|
imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever)
|
|
with imscope.window("Prompt Presets Manager", app.show_preset_manager_window) as (opened, visible):
|
|
app.show_preset_manager_window = visible
|
|
if opened: render_preset_manager_content(app, is_embedded=is_embedded)
|
|
else:
|
|
render_preset_manager_content(app, is_embedded=is_embedded)
|
|
|
|
def render_tool_preset_manager_content(app: App, is_embedded: bool = False) -> None:
|
|
avail = imgui.get_content_region_avail()
|
|
if not hasattr(app, "_tool_split_v"): app._tool_split_v = 0.4
|
|
if not hasattr(app, "_bias_split_v"): app._bias_split_v = 0.6
|
|
if not hasattr(app, "_tool_list_open"): app._tool_list_open = True
|
|
if not hasattr(app, "_bias_list_open"): app._bias_list_open = True
|
|
if not hasattr(app, "_bias_weights_open"): app._bias_weights_open = True
|
|
if not hasattr(app, "_bias_cats_open"): app._bias_cats_open = True
|
|
|
|
if imgui.begin_table("tp_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v):
|
|
imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200)
|
|
imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row()
|
|
|
|
# Left Sidebar
|
|
imgui.table_next_column()
|
|
imgui.begin_child("tp_list_pane", imgui.ImVec2(0, 0), False)
|
|
if True:
|
|
if imgui.button("New Preset", imgui.ImVec2(-1, 0)):
|
|
app._editing_tool_preset_name = ""; app._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES}
|
|
app._editing_tool_preset_scope = "project"; app._selected_tool_preset_idx = -1
|
|
imgui.separator()
|
|
preset_names = sorted(app.controller.tool_presets.keys())
|
|
for i, name in enumerate(preset_names):
|
|
if name and imgui.selectable(f"{name}##tp_{i}", app._selected_tool_preset_idx == i)[0]:
|
|
app._selected_tool_preset_idx = i; app._editing_tool_preset_name = name
|
|
preset = app.controller.tool_presets[name]
|
|
app._editing_tool_preset_categories = {cat: copy.deepcopy(tools) for cat, tools in preset.categories.items()}
|
|
imgui.end_child()
|
|
|
|
# Right Editor
|
|
imgui.table_next_column()
|
|
avail_r = imgui.get_content_region_avail()
|
|
imgui.begin_child("tp_editor_content", imgui.ImVec2(0, avail_r.y - 45), False)
|
|
if True:
|
|
p_name = app._editing_tool_preset_name or "(New Tool Preset)"
|
|
imgui.text_colored(C_IN(), f"Editing Tool Preset: {p_name}"); imgui.separator()
|
|
|
|
if imgui.begin_table("tp_meta", 2):
|
|
imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 80); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, app._editing_tool_preset_name = imgui.input_text("##etpn", app._editing_tool_preset_name)
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column()
|
|
if imgui.radio_button("Global", app._editing_tool_preset_scope == "global"): app._editing_tool_preset_scope = "global"
|
|
imgui.same_line();
|
|
if imgui.radio_button("Project", app._editing_tool_preset_scope == "project"): app._editing_tool_preset_scope = "project"
|
|
imgui.end_table()
|
|
|
|
rem_y = imgui.get_content_region_avail().y - 80
|
|
if app._tool_list_open and app._bias_list_open: h1, h2 = rem_y * app._tool_split_v, rem_y - (rem_y * app._tool_split_v) - 10
|
|
elif app._tool_list_open: h1, h2 = rem_y, 0
|
|
elif app._bias_list_open: h1, h2 = 0, rem_y
|
|
else: h1, h2 = 0, 0
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 4))
|
|
opened_t = imgui.collapsing_header("Categories & Tools", imgui.TreeNodeFlags_.default_open)
|
|
if opened_t != app._tool_list_open: app._tool_list_open = opened_t
|
|
if app._tool_list_open:
|
|
imgui.text("Filter:"); imgui.same_line()
|
|
cat_opts = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys()))
|
|
f_idx = cat_opts.index(app.ui_tool_filter_category) if app.ui_tool_filter_category in cat_opts else 0
|
|
imgui.set_next_item_width(200); ch_cat, next_f_idx = imgui.combo("##tp_filter", f_idx, cat_opts)
|
|
if ch_cat: app.ui_tool_filter_category = cat_opts[next_f_idx]
|
|
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)):
|
|
try:
|
|
p = models.BiasProfile(name=app._editing_bias_profile_name, tool_weights=app._editing_bias_profile_tool_weights, category_multipliers=app._editing_bias_profile_category_multipliers)
|
|
app.controller._cb_save_bias_profile(p, app._editing_tool_preset_scope); app.ai_status = f"Saved: {p.name}"
|
|
except Exception as e: app.ai_status = f"Error: {e}"
|
|
imgui.end_child()
|
|
imgui.end_table()
|
|
imgui.end_child()
|
|
imgui.end_child()
|
|
|
|
# --- Footer Buttons ---
|
|
imgui.separator()
|
|
if imgui.button("Save##tp", imgui.ImVec2(100, 0)):
|
|
if app._editing_tool_preset_name.strip(): app.controller._cb_save_tool_preset(app._editing_tool_preset_name.strip(), app._editing_tool_preset_categories, app._editing_tool_preset_scope); app.ai_status = f"Saved: {app._editing_tool_preset_name}"
|
|
imgui.same_line()
|
|
if imgui.button("Delete##tp", imgui.ImVec2(100, 0)):
|
|
if app._editing_tool_preset_name: app.controller._cb_delete_tool_preset(app._editing_tool_preset_name, app._editing_tool_preset_scope); app._editing_tool_preset_name = ""; app._selected_tool_preset_idx = -1
|
|
imgui.same_line()
|
|
if not is_embedded:
|
|
if imgui.button("Close##tp", imgui.ImVec2(100, 0)): app.show_tool_preset_manager_window = False
|
|
imgui.end_table()
|
|
|
|
def render_tool_preset_manager_window(app: App, is_embedded: bool = False) -> None:
|
|
if not app.show_tool_preset_manager_window and not is_embedded: return
|
|
if not is_embedded:
|
|
imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever)
|
|
with imscope.window("Tool Preset Manager", app.show_tool_preset_manager_window) as (opened, visible):
|
|
app.show_tool_preset_manager_window = visible
|
|
if opened: render_tool_preset_manager_content(app, is_embedded=is_embedded)
|
|
else:
|
|
render_preset_manager_content(app, is_embedded=is_embedded)
|
|
|
|
def render_persona_editor_window(app: App, is_embedded: bool = False) -> None:
|
|
if not app.show_persona_editor_window and not is_embedded: return
|
|
if not is_embedded:
|
|
imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever)
|
|
opened, app.show_persona_editor_window = imgui.begin("Persona Editor", app.show_persona_editor_window)
|
|
if not opened:
|
|
imgui.end(); return
|
|
|
|
if imgui.begin_table("persona_main_split", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v):
|
|
# --- Left Sidebar ---
|
|
imgui.table_setup_column("List", imgui.TableColumnFlags_.width_fixed, 200)
|
|
imgui.table_setup_column("Editor", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row()
|
|
|
|
imgui.table_next_column()
|
|
imgui.begin_child("persona_list_pane", imgui.ImVec2(0, 0), False)
|
|
if True:
|
|
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
|
app._editing_persona_name = ""; app._editing_persona_system_prompt = ""
|
|
app._editing_persona_tool_preset_id = ""; app._editing_persona_bias_profile_id = ""
|
|
app._editing_persona_context_preset_id = ""
|
|
app._editing_persona_aggregation_strategy = ""
|
|
app._editing_persona_preferred_models_list = [{"provider": app.current_provider, "model": app.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
|
|
app._editing_persona_scope = "project"; app._editing_persona_is_new = True
|
|
imgui.separator()
|
|
personas = getattr(app.controller, 'personas', {})
|
|
for name in sorted(personas.keys()):
|
|
if name and imgui.selectable(f"{name}##p_list", name == app._editing_persona_name and not getattr(app, '_editing_persona_is_new', False))[0]:
|
|
p = personas[name]; app._editing_persona_name = p.name; app._editing_persona_system_prompt = p.system_prompt or ""
|
|
app._editing_persona_tool_preset_id = p.tool_preset or ""; app._editing_persona_bias_profile_id = p.bias_profile or ""
|
|
app._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or ""
|
|
app._editing_persona_aggregation_strategy = getattr(p, 'aggregation_strategy', '') or ""
|
|
import copy; app._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
|
|
app._editing_persona_scope = app.controller.persona_manager.get_persona_scope(p.name); app._editing_persona_is_new = False
|
|
imgui.end_child()
|
|
|
|
# --- Right Editor ---
|
|
imgui.table_next_column()
|
|
avail = imgui.get_content_region_avail()
|
|
imgui.begin_child("persona_editor_content", imgui.ImVec2(0, avail.y - 45), False)
|
|
if True:
|
|
header_text = "New Persona" if getattr(app, '_editing_persona_is_new', True) else f"Editing Persona: {app._editing_persona_name}"
|
|
imgui.text_colored(C_IN(), header_text); imgui.separator()
|
|
|
|
if imgui.begin_table("p_meta", 2):
|
|
imgui.table_setup_column("L", imgui.TableColumnFlags_.width_fixed, 60); imgui.table_setup_column("F", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text("Name:"); imgui.table_next_column(); imgui.set_next_item_width(-1); _, app._editing_persona_name = imgui.input_text("##pname", app._editing_persona_name, 128)
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text("Scope:"); imgui.table_next_column()
|
|
if imgui.radio_button("Global##pscope", getattr(app, '_editing_persona_scope', 'project') == "global"): app._editing_persona_scope = "global"
|
|
imgui.same_line();
|
|
if imgui.radio_button("Project##pscope", getattr(app, '_editing_persona_scope', 'project') == "project"): app._editing_persona_scope = "project"
|
|
imgui.end_table()
|
|
|
|
rem_y = imgui.get_content_region_avail().y - 100
|
|
if app._persona_models_open and app._persona_prompt_open: h1, h2 = rem_y * app._persona_split_v, rem_y - (rem_y * app._persona_split_v) - 10
|
|
elif app._persona_models_open: h1, h2 = rem_y, 0
|
|
elif app._persona_prompt_open: h1, h2 = 0, rem_y
|
|
else: h1, h2 = 0, 0
|
|
|
|
imgui.dummy(imgui.ImVec2(0, 4))
|
|
opened_models = imgui.collapsing_header("Preferred Models", imgui.TreeNodeFlags_.default_open)
|
|
if opened_models != app._persona_models_open: app._persona_models_open = opened_models
|
|
|
|
if app._persona_models_open:
|
|
imgui.begin_child("pref_models_scroll", imgui.ImVec2(0, h1), True)
|
|
if True:
|
|
to_remove = []
|
|
providers = models.PROVIDERS
|
|
if not hasattr(app, '_persona_pref_models_expanded'): app._persona_pref_models_expanded = {}
|
|
for i, entry in enumerate(app._editing_persona_preferred_models_list):
|
|
imgui.push_id(f"pref_model_{i}")
|
|
prov, mod, is_expanded = entry.get("provider", "Unknown"), entry.get("model", "Unknown"), app._persona_pref_models_expanded.get(i, False)
|
|
if imgui.button("-" if is_expanded else "+"): app._persona_pref_models_expanded[i] = not is_expanded
|
|
imgui.same_line(); imgui.text(f"{i+1}."); imgui.same_line(); imgui.text_colored(C_LBL(), f"{prov}"); imgui.same_line(); imgui.text("-"); imgui.same_line(); imgui.text_colored(C_IN(), f"{mod}")
|
|
if not is_expanded:
|
|
imgui.same_line(); summary = f" (T:{entry.get('temperature', 0.7):.1f}, P:{entry.get('top_p', 1.0):.2f}, M:{entry.get('max_output_tokens', 0)})"
|
|
imgui.text_colored(C_SUB(), summary)
|
|
imgui.same_line(imgui.get_content_region_avail().x - 30);
|
|
if imgui.button("x"): to_remove.append(i)
|
|
if is_expanded:
|
|
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():
|
|
try:
|
|
import copy; persona = models.Persona(name=app._editing_persona_name.strip(), system_prompt=app._editing_persona_system_prompt, tool_preset=app._editing_persona_tool_preset_id or None, bias_profile=app._editing_persona_bias_profile_id or None, context_preset=app._editing_persona_context_preset_id or None, aggregation_strategy=app._editing_persona_aggregation_strategy or None, preferred_models=copy.deepcopy(app._editing_persona_preferred_models_list))
|
|
app.controller._cb_save_persona(persona, getattr(app, '_editing_persona_scope', 'project')); app.ai_status = f"Saved: {persona.name}"
|
|
except Exception as e: app.ai_status = f"Error: {e}"
|
|
else: app.ai_status = "Name required"
|
|
imgui.same_line();
|
|
if imgui.button("Delete##pers", imgui.ImVec2(100, 0)):
|
|
if not getattr(app, '_editing_persona_is_new', True) and app._editing_persona_name:
|
|
app.controller._cb_delete_persona(app._editing_persona_name, getattr(app, '_editing_persona_scope', 'project'))
|
|
app._editing_persona_name = ""; app._editing_persona_is_new = True
|
|
if not is_embedded:
|
|
imgui.same_line()
|
|
if imgui.button("Close##pers", imgui.ImVec2(100, 0)):
|
|
app.show_persona_editor_window = False
|
|
imgui.end_table()
|
|
|
|
if not is_embedded:
|
|
imgui.end()
|
|
|
|
#endregion: AI Settings
|
|
|
|
#region: Context Management
|
|
|
|
def render_files_and_media(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_fast_render.py:test_render_files_and_media_fast]
|
|
"""
|
|
if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open):
|
|
with imscope.group():
|
|
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()
|
|
|
|
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)
|
|
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 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)
|
|
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(fpath)
|
|
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()
|
|
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)
|
|
|
|
if imgui.button("Add Screenshots##adds"):
|
|
r = hide_tk_root(); paths = filedialog.askopenfilenames(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")]); r.destroy()
|
|
for p in paths:
|
|
if p not in app.screenshots: app.screenshots.append(p)
|
|
return
|
|
|
|
def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None:
|
|
imgui.text("Batch:")
|
|
for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]:
|
|
if imgui.button(f"{mode.capitalize()}##batch"):
|
|
for f in app.context_files:
|
|
f_path = f.path if hasattr(f, "path") else str(f)
|
|
if f_path in app.ui_selected_context_files: f.view_mode = mode
|
|
imgui.same_line()
|
|
if imgui.button("Sel All##selall"):
|
|
for f in app.context_files:
|
|
f_path = f.path if hasattr(f, "path") else str(f)
|
|
app.ui_selected_context_files.add(f_path)
|
|
imgui.same_line()
|
|
if imgui.button("Unsel All##unselall"): app.ui_selected_context_files.clear()
|
|
imgui.same_line()
|
|
if imgui.button("Add Files##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:
|
|
try:
|
|
app.controller.context_files = app.context_files
|
|
app.context_preview_text = app.controller._do_generate()[0]
|
|
except Exception as e:
|
|
import traceback
|
|
err = traceback.format_exc()
|
|
app.context_preview_text = f"# Error generating preview\n\n```python\n{err}\n```"
|
|
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:
|
|
"""
|
|
[C: tests/test_auto_slices.py:test_add_selected_triggers_auto_slices]
|
|
"""
|
|
if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
|
imgui.text("Select files from project to add to context:")
|
|
imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True)
|
|
if True:
|
|
# Create a temporary selection set if not initialized
|
|
if not hasattr(app, '_ui_picker_selected'): app._ui_picker_selected = set()
|
|
for f in app.files:
|
|
fpath = f.path if hasattr(f, 'path') else str(f)
|
|
# Skip if already in context
|
|
if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files):
|
|
continue
|
|
is_sel = fpath in app._ui_picker_selected
|
|
clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel)
|
|
if clicked:
|
|
if new_sel:
|
|
app._ui_picker_selected.add(fpath)
|
|
else:
|
|
app._ui_picker_selected.discard(fpath)
|
|
imgui.end_child()
|
|
imgui.separator()
|
|
|
|
if imgui.button("Add Selected", imgui.ImVec2(120, 0)):
|
|
for fpath in app._ui_picker_selected:
|
|
f_item = models.FileItem(path=fpath)
|
|
app.context_files.append(f_item)
|
|
app._populate_auto_slices(f_item)
|
|
app._ui_picker_selected.clear()
|
|
imgui.close_current_popup()
|
|
|
|
imgui.same_line()
|
|
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
|
if hasattr(app, '_ui_picker_selected'):
|
|
app._ui_picker_selected.clear()
|
|
imgui.close_current_popup()
|
|
imgui.end_popup()
|
|
|
|
def render_context_composition_panel(app: App) -> None:
|
|
"""
|
|
[C: tests/test_auto_slices.py:test_add_all_triggers_auto_slices, tests/test_gui_fast_render.py:test_render_context_composition_panel_fast]
|
|
"""
|
|
if imgui.collapsing_header("Context Composition##panel"):
|
|
total_lines, total_ast = app._update_context_file_stats()
|
|
render_context_batch_actions(app, total_lines, total_ast)
|
|
render_context_files_table(app)
|
|
|
|
imgui.separator()
|
|
render_context_presets(app)
|
|
imgui.separator()
|
|
if imgui.collapsing_header("Screenshots"):
|
|
render_context_screenshots(app)
|
|
|
|
def render_ast_inspector_modal(app: App) -> None:
|
|
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 = ""
|
|
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)
|
|
|
|
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)
|
|
except Exception as e:
|
|
outline = f"Error fetching outline: {e}"
|
|
|
|
app._cached_ast_nodes = []
|
|
import re
|
|
pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)')
|
|
stack = [] # (indent, name)
|
|
for line in outline.splitlines():
|
|
m = pattern.match(line)
|
|
if m:
|
|
indent_str, kind, name, start_ln, end_ln = m.groups()
|
|
indent = len(indent_str)
|
|
while stack and stack[-1][0] >= indent: stack.pop()
|
|
stack.append((indent, name))
|
|
full_path = '::'.join([s[1] for s in stack])
|
|
app._cached_ast_nodes.append({
|
|
'indent': indent,
|
|
'kind': kind,
|
|
'name': name,
|
|
'full_path': full_path,
|
|
'start_line': int(start_ln),
|
|
'end_line': int(end_ln)
|
|
})
|
|
try:
|
|
content = mcp_client.read_file(f_path)
|
|
app._cached_ast_file_lines = content.splitlines()
|
|
app.text_viewer_content = content
|
|
except Exception:
|
|
app._cached_ast_file_lines = ["Error loading file content."]
|
|
app.text_viewer_content = "Error loading file content."
|
|
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:
|
|
for node in app._cached_ast_nodes:
|
|
indent = node['indent']
|
|
kind = node['kind']
|
|
name = node['name']
|
|
full_path = node['full_path']
|
|
|
|
imgui.dummy(imgui.ImVec2(indent * 10, 0))
|
|
imgui.same_line()
|
|
imgui.text(f"[{kind}] {name}")
|
|
|
|
if imgui.is_item_hovered():
|
|
app._hovered_ast_node = full_path
|
|
|
|
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(imgui.get_window_width() - btn_width)
|
|
else: imgui.same_line()
|
|
|
|
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:
|
|
if app._show_save_workspace_profile_modal:
|
|
imgui.open_popup("Save Workspace Profile")
|
|
|
|
if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
|
imgui.text("Name:")
|
|
_, app._new_workspace_profile_name = imgui.input_text("##profile_name", app._new_workspace_profile_name)
|
|
|
|
imgui.text("Scope:")
|
|
if imgui.radio_button("Project", app._new_workspace_profile_scope == "project"): app._new_workspace_profile_scope = "project"
|
|
imgui.same_line()
|
|
if imgui.radio_button("Global", app._new_workspace_profile_scope == "global"): app._new_workspace_profile_scope = "global"
|
|
|
|
imgui.separator()
|
|
if imgui.button("Save", (120, 0)):
|
|
if app._new_workspace_profile_name.strip():
|
|
app.controller._cb_save_workspace_profile(app._new_workspace_profile_name, app._new_workspace_profile_scope)
|
|
app._show_save_workspace_profile_modal = False
|
|
imgui.close_current_popup()
|
|
|
|
imgui.same_line()
|
|
if imgui.button("Cancel", (120, 0)):
|
|
app._show_save_workspace_profile_modal = False
|
|
imgui.close_current_popup()
|
|
|
|
imgui.end_popup()
|
|
|
|
def render_context_presets_panel(app: App) -> None:
|
|
imgui.text_colored(C_IN(), "Context Presets")
|
|
imgui.separator()
|
|
changed, new_name = imgui.input_text("Preset Name##new_ctx", app.ui_new_context_preset_name)
|
|
if changed: app.ui_new_context_preset_name = new_name
|
|
imgui.same_line()
|
|
if imgui.button("Save Current"):
|
|
if app.ui_new_context_preset_name.strip():
|
|
app.save_context_preset(app.ui_new_context_preset_name.strip())
|
|
|
|
imgui.separator()
|
|
presets = app.controller.project.get('context_presets', {})
|
|
for name in sorted(presets.keys()):
|
|
preset = presets[name]
|
|
n_files = len(preset.get('files', []))
|
|
n_shots = len(preset.get('screenshots', []))
|
|
imgui.text(f"{name} ({n_files} files, {n_shots} shots)")
|
|
imgui.same_line()
|
|
if imgui.button(f"Load##{name}"): app.load_context_preset(name)
|
|
imgui.same_line()
|
|
if imgui.button(f"Delete##{name}"): app.delete_context_preset(name)
|
|
|
|
def render_context_screenshots(app: App) -> None:
|
|
for i, s in enumerate(app.screenshots): imgui.text(s)
|
|
|
|
def render_context_files_table(app: App) -> None:
|
|
imgui.dummy(imgui.ImVec2(0, 4))
|
|
grouped_files = aggregate.group_files_by_dir(app.context_files)
|
|
|
|
with imscope.table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders) as active:
|
|
if active:
|
|
imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200)
|
|
imgui.table_headers_row()
|
|
|
|
file_indices = {id(f): idx for idx, f in enumerate(app.context_files)}
|
|
|
|
for dir_name, g_files in grouped_files.items():
|
|
imgui.table_next_row()
|
|
imgui.table_set_column_index(0)
|
|
with imscope.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open) as is_open:
|
|
imgui.table_set_column_index(1)
|
|
if is_open:
|
|
for f_item in g_files:
|
|
i = file_indices[id(f_item)]
|
|
imgui.table_next_row()
|
|
imgui.table_set_column_index(0)
|
|
|
|
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
|
|
is_sel = f_path in app.ui_selected_context_files
|
|
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 = app._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0})
|
|
f_name = os.path.basename(f_path)
|
|
imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})")
|
|
if 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:
|
|
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", ""))
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
app.controller.context_files = app.context_files
|
|
res = app.controller._do_generate()
|
|
app.context_preview_text = res[0]
|
|
except Exception:
|
|
app.context_preview_text = "Error generating context preview."
|
|
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:
|
|
_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:
|
|
_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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
|
|
def render_prior_session_view(app: App) -> None:
|
|
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:
|
|
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..."); imgui.same_line()
|
|
|
|
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.
|
|
[C: src/gui_2.py:render_warmup_status_indicator, src/gui_2.py:App._post_init, tests/test_gui_warmup_indicator.py:test_callback_sets_timestamp]
|
|
"""
|
|
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))
|
|
except Exception: pass
|
|
|
|
def render_warmup_status_indicator(app: App) -> None:
|
|
"""
|
|
Renders a per-frame warmup status line. Shows the progress of
|
|
AppController's background warmup (Phase 2 of
|
|
startup_speedup_20260606). Hidden when the controller has no warmup
|
|
or warmup is done with no failures. Shows a transient "ready" tag
|
|
for 3 seconds after completion.
|
|
[C: src/gui_2.py:App._post_init, src/gui_2.py:render_main_interface, tests/test_gui_warmup_indicator.py:test_render_warmup_indicator_function_exists, tests/test_gui_warmup_indicator.py:test_callback_sets_timestamp]
|
|
"""
|
|
controller = getattr(app, "controller", None)
|
|
if controller is None: return
|
|
if not hasattr(controller, "warmup_status"): return
|
|
try:
|
|
status = controller.warmup_status()
|
|
except Exception: return
|
|
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.
|
|
[C: tests/test_gui_synthesis.py:test_render_synthesis_panel]
|
|
"""
|
|
imgui.text("Select takes to synthesize:")
|
|
discussions = app.project.get('discussion', {}).get('discussions', {})
|
|
if not hasattr(app, 'ui_synthesis_selected_takes'): app.ui_synthesis_selected_takes = {name: False for name in discussions}
|
|
if not hasattr(app, 'ui_synthesis_prompt'): app.ui_synthesis_prompt = ""
|
|
for name in discussions: _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False))
|
|
imgui.spacing()
|
|
imgui.text("Synthesis Prompt:")
|
|
_, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100))
|
|
if imgui.button("Generate Synthesis"):
|
|
selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel]
|
|
if len(selected) > 1:
|
|
discussions_dict = app.project.get('discussion', {}).get('discussions', {})
|
|
takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected}
|
|
diff_text = synthesis_formatter.format_takes_diff(takes_dict)
|
|
prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}"
|
|
|
|
new_name = "synthesis_take"
|
|
counter = 1
|
|
while new_name in discussions_dict:
|
|
new_name = f"synthesis_take_{counter}"
|
|
counter += 1
|
|
|
|
app._create_discussion(new_name)
|
|
with app._disc_entries_lock: app.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()})
|
|
app._handle_generate_send()
|
|
|
|
def render_comms_history_panel(app: App) -> None:
|
|
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)
|
|
imgui.text_colored(d_col_fn(), direction); imgui.same_line()
|
|
k_col_fn = KIND_COLORS.get(kind, C_VAL)
|
|
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:
|
|
imgui.text("Takes & Synthesis")
|
|
imgui.separator()
|
|
discussions = app.project.get('discussion', {}).get('discussions', {})
|
|
if not hasattr(app, 'ui_synthesis_selected_takes'):
|
|
app.ui_synthesis_selected_takes = {name: False for name in discussions}
|
|
if not hasattr(app, 'ui_synthesis_prompt'):
|
|
app.ui_synthesis_prompt = ""
|
|
if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
|
|
imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80)
|
|
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150)
|
|
imgui.table_headers_row()
|
|
for name, disc in list(discussions.items()):
|
|
imgui.table_next_row()
|
|
imgui.table_set_column_index(0)
|
|
is_active = name == app.active_discussion
|
|
if is_active:
|
|
imgui.text_colored(C_IN(), name)
|
|
else:
|
|
imgui.text(name)
|
|
imgui.table_set_column_index(1)
|
|
history = disc.get('history', [])
|
|
imgui.text(f"{len(history)}")
|
|
imgui.table_set_column_index(2)
|
|
if imgui.button(f"Switch##{name}"):
|
|
app._switch_discussion(name)
|
|
imgui.same_line()
|
|
if name != "main" and imgui.button(f"Delete##{name}"):
|
|
del discussions[name]
|
|
imgui.end_table()
|
|
imgui.separator()
|
|
imgui.text("Synthesis")
|
|
imgui.text("Select takes to synthesize:")
|
|
for name in discussions:
|
|
_, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False))
|
|
imgui.spacing()
|
|
imgui.text("Synthesis Prompt:")
|
|
_, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100))
|
|
if imgui.button("Generate Synthesis"):
|
|
selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel]
|
|
if len(selected) > 1:
|
|
from src import synthesis_formatter
|
|
takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected}
|
|
diff_text = synthesis_formatter.format_takes_diff(takes_dict)
|
|
prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}"
|
|
new_name = "synthesis_take"
|
|
counter = 1
|
|
while new_name in discussions:
|
|
new_name = f"synthesis_take_{counter}"
|
|
counter += 1
|
|
app._create_discussion(new_name)
|
|
with app._disc_entries_lock:
|
|
app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()})
|
|
app._handle_generate_send()
|
|
|
|
def render_discussion_entries(app: App) -> None:
|
|
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:
|
|
if imgui.button("+ Entry"): app.disc_entries.append({"role": app.disc_roles[0] if app.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
|
|
imgui.same_line()
|
|
if imgui.button("-All"):
|
|
for e in app.disc_entries: e["collapsed"] = True
|
|
imgui.same_line()
|
|
if imgui.button("+All"):
|
|
for e in app.disc_entries: e["collapsed"] = False
|
|
imgui.same_line()
|
|
if imgui.button("Clear All"): app.disc_entries.clear()
|
|
imgui.same_line()
|
|
if imgui.button("Save"): app._flush_to_project(); app._flush_to_config(); models.save_config(app.config); app.ai_status = "discussion saved"
|
|
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:
|
|
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:
|
|
"""
|
|
[C: tests/test_discussion_takes_gui.py:test_render_discussion_tabs, tests/test_discussion_takes_gui.py:test_switching_discussion_via_tabs, tests/test_gui_discussion_tabs.py:test_discussion_tabs_rendered, tests/test_gui_fast_render.py:test_render_discussion_panel_fast, tests/test_gui_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup]
|
|
"""
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel")
|
|
render_thinking_indicator(app)
|
|
|
|
if app.is_viewing_prior_session:
|
|
render_prior_session_view(app)
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel")
|
|
return
|
|
|
|
render_discussion_selector(app)
|
|
|
|
if not app.is_viewing_prior_session:
|
|
imgui.separator(); render_discussion_entry_controls(app)
|
|
imgui.separator(); render_discussion_roles(app)
|
|
imgui.separator(); render_discussion_entries(app)
|
|
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel")
|
|
return
|
|
|
|
def render_discussion_roles(app: App) -> None:
|
|
if imgui.collapsing_header("Roles"):
|
|
with imscope.child("roles_scroll", size_y=100, flags=True):
|
|
for i, r in enumerate(list(app.disc_roles)):
|
|
with imscope.id(f"role_{i}"):
|
|
if imgui.button("X"): app.disc_roles.pop(i); break
|
|
imgui.same_line(); imgui.text(r)
|
|
ch, app.ui_disc_new_role_input = imgui.input_text("##new_role", app.ui_disc_new_role_input); imgui.same_line()
|
|
if imgui.button("Add"):
|
|
r = app.ui_disc_new_role_input.strip()
|
|
if r and r not in app.disc_roles: app.disc_roles.append(r); app.ui_disc_new_role_input = ""
|
|
return
|
|
|
|
def render_discussion_selector(app: App) -> None:
|
|
if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
|
return
|
|
names = app._get_discussion_names(); grouped = {}
|
|
for name in names:
|
|
base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name)
|
|
active_base = app.active_discussion.split("_take_")[0]
|
|
if active_base not in grouped: active_base = names[0] if names else ""
|
|
base_names = sorted(grouped.keys())
|
|
if imgui.begin_combo("##disc_sel", active_base):
|
|
for bname in base_names:
|
|
is_selected = (bname == active_base)
|
|
if imgui.selectable(bname, is_selected)[0]:
|
|
target = bname if bname in names else grouped[bname][0]
|
|
if target != app.active_discussion: app._switch_discussion(target)
|
|
if is_selected: imgui.set_item_default_focus()
|
|
imgui.end_combo()
|
|
active_base = app.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, [])
|
|
if imgui.begin_tab_bar("discussion_takes_tabs"):
|
|
for take_name in current_takes:
|
|
label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title()
|
|
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:
|
|
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:
|
|
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
|
|
ch1, app.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", app.ui_separate_tool_calls_panel)
|
|
if ch1: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel
|
|
imgui.same_line()
|
|
ch2, app.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", app.ui_separate_usage_analytics)
|
|
if ch2: app.show_windows["Usage Analytics"] = app.ui_separate_usage_analytics
|
|
imgui.same_line()
|
|
ch3, app.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', app.ui_separate_external_tools)
|
|
if ch3: app.show_windows['External Tools'] = app.ui_separate_external_tools
|
|
imgui.pop_style_var()
|
|
show_tc_tab, show_usage_tab = not app.ui_separate_tool_calls_panel, not app.ui_separate_usage_analytics
|
|
with imscope.tab_bar("ops_tabs"):
|
|
with imscope.tab_item("Comms History") as (exp, _):
|
|
if exp: render_comms_history_panel(app)
|
|
if show_tc_tab:
|
|
with imscope.tab_item("Tool Calls") as (exp, _):
|
|
if exp: render_tool_calls_panel(app)
|
|
if show_usage_tab:
|
|
with imscope.tab_item("Usage Analytics") as (exp, _):
|
|
if exp: render_usage_analytics_panel(app)
|
|
if not app.ui_separate_external_tools:
|
|
with imscope.tab_item("External Tools") as (exp, _):
|
|
if exp:
|
|
render_external_tools_panel(app)
|
|
imgui.separator(); imgui.text("")
|
|
try: render_external_editor_panel(app)
|
|
except Exception as e: imgui.text_colored(theme.get_color("status_error"), f"Error: {str(e)}")
|
|
with imscope.tab_item("Workspace Layouts") as (exp, _):
|
|
if exp:
|
|
imgui.text("Experimental: Auto-switch layout by Tier")
|
|
ch, app.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", app.controller.ui_auto_switch_layout)
|
|
if app.controller.ui_auto_switch_layout:
|
|
imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)")
|
|
profiles = [""] + [p.name for p in app.controller.workspace_profiles.values()]
|
|
for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]:
|
|
curr = app.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0
|
|
ch_combo, new_idx = imgui.combo(t, idx, profiles)
|
|
if ch_combo: app.controller.ui_tier_layout_bindings[t] = profiles[new_idx]
|
|
with imscope.tab_item("Vendor State") as (exp, _):
|
|
if exp: render_vendor_state(app)
|
|
|
|
def render_vendor_state(app: App) -> None: # TODO(Ed): Shouldn't this just be a part of usage analytics? We can show all used vendors at once...
|
|
"""Render the Operations Hub > Vendor State panel.
|
|
[C: src/vendor_state.py:get_vendor_state]
|
|
"""
|
|
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:
|
|
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:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_response_panel")
|
|
if app._trigger_blink:
|
|
app._trigger_blink = False
|
|
app._is_blinking = True
|
|
app._blink_start_time = time.time()
|
|
try:
|
|
imgui.set_window_focus("Response") # type: ignore[call-arg]
|
|
except:
|
|
pass
|
|
is_blinking = False
|
|
blink_color = 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:
|
|
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:
|
|
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."""
|
|
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)
|
|
try:
|
|
app._text_viewer_editor.set_text(app.text_viewer_content)
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_text_viewer_ced")
|
|
app._text_viewer_editor.render(f"##ced_{app.text_viewer_title}", imgui.ImVec2(-1, -1))
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_text_viewer_ced")
|
|
except Exception as e: imgui.text_colored(theme.get_color("status_error"), f"CED Error: {e}"); imgui.text_unformatted(app.text_viewer_content)
|
|
else:
|
|
with imscope.child("tv_scroll", -1, -1, True):
|
|
if app.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
|
imgui.text_unformatted(app.text_viewer_content)
|
|
if app.text_viewer_wrap: imgui.pop_text_wrap_pos()
|
|
imgui.end()
|
|
|
|
def render_patch_modal(app: App) -> None:
|
|
if not app._show_patch_modal:
|
|
return
|
|
imgui.open_popup("Apply Patch?")
|
|
with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _):
|
|
if opened:
|
|
p_min = imgui.get_window_pos()
|
|
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
|
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
|
|
|
imgui.text_colored(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:
|
|
from src.external_editor import get_default_launcher
|
|
imgui.text("External Editor for Diff Viewing")
|
|
imgui.separator()
|
|
try:
|
|
launcher = get_default_launcher()
|
|
editors = launcher.config.editors
|
|
default_name = launcher.config.default_editor
|
|
if not editors:
|
|
imgui.text_colored(C_REQ(), " No editors configured")
|
|
imgui.text("")
|
|
imgui.text("Add editors in config.toml:")
|
|
imgui.text(" [tools.text_editors.vscode]")
|
|
imgui.text(' path = "C:\\\\path\\\\to\\\\code.exe"')
|
|
imgui.text(' diff_args = ["--diff"]')
|
|
imgui.text("")
|
|
imgui.text(" [tools.text_editors.notepadpp]")
|
|
imgui.text(' path = "C:\\\\path\\\\to\\\\notepad++.exe"')
|
|
imgui.text(' diff_args = ["-multiInst", "-nosession"]')
|
|
imgui.text("")
|
|
imgui.text("Then set default in [tools.default_editor]")
|
|
else:
|
|
imgui.text("Default Editor:")
|
|
editor_names = sorted(list(editors.keys()))
|
|
if default_name and default_name in editor_names: current_idx = editor_names.index(default_name)
|
|
else: current_idx = 0
|
|
changed, new_idx = imgui.combo("##editor_combo", current_idx, editor_names)
|
|
if changed: app._set_external_editor_default(editor_names[new_idx])
|
|
imgui.text("")
|
|
imgui.text("Configured Editors:")
|
|
imgui.separator()
|
|
for name in editor_names:
|
|
editor = editors.get(name)
|
|
if not editor: continue
|
|
is_default = name == default_name
|
|
marker = " (default)" if is_default else ""
|
|
if is_default: imgui.text_colored(C_IN(), f" {name}{marker}")
|
|
else: imgui.text(f" {name}{marker}")
|
|
imgui.text(f" {editor.path}")
|
|
if editor.diff_args: imgui.textDisabled(f" diff: {editor.diff_args}")
|
|
imgui.text("")
|
|
imgui.text("Config: config.toml [tools.text_editors]")
|
|
imgui.text("Override: manual_slop.toml default_editor")
|
|
except Exception as e:
|
|
imgui.text_colored(C_TC(), f"Error: {str(e)}")
|
|
|
|
def render_approve_script_modal(app: App) -> None:
|
|
"""Renders the modal dialog for approving AI-generated PowerShell scripts."""
|
|
with app._pending_dialog_lock:
|
|
dlg = app._pending_dialog
|
|
if dlg:
|
|
if not app._pending_dialog_open:
|
|
imgui.open_popup("Approve PowerShell Command")
|
|
app._pending_dialog_open = True
|
|
else:
|
|
app._pending_dialog_open = False
|
|
|
|
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:
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_theme_panel")
|
|
exp, opened = imgui.begin("Theme", app.show_windows["Theme"])
|
|
app.show_windows["Theme"] = bool(opened)
|
|
if exp:
|
|
imgui.text("Palette")
|
|
cp = theme.get_current_palette()
|
|
if imgui.begin_combo("##pal", cp):
|
|
for p in theme.get_palette_names():
|
|
if imgui.selectable(p, p == cp)[0]:
|
|
theme.apply(p)
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
imgui.end_combo()
|
|
|
|
imgui.separator()
|
|
ch1, app.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", app.ui_separate_message_panel)
|
|
ch2, app.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", app.ui_separate_response_panel)
|
|
ch3, app.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", app.ui_separate_tool_calls_panel)
|
|
if ch1: app.show_windows["Message"] = app.ui_separate_message_panel
|
|
if ch2: app.show_windows["Response"] = app.ui_separate_response_panel
|
|
if ch3: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel
|
|
imgui.separator()
|
|
imgui.text("Font")
|
|
imgui.push_item_width(-150)
|
|
ch, path = imgui.input_text("##fontp", theme.get_current_font_path())
|
|
imgui.pop_item_width()
|
|
if ch: theme._current_font_path = path
|
|
imgui.same_line()
|
|
if imgui.button("Browse##font"):
|
|
r = hide_tk_root()
|
|
p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")])
|
|
r.destroy()
|
|
if p: theme._current_font_path = p
|
|
imgui.text("Size (px)")
|
|
imgui.same_line()
|
|
imgui.push_item_width(100)
|
|
ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f")
|
|
if ch: theme._current_font_size = size
|
|
imgui.pop_item_width()
|
|
imgui.same_line()
|
|
if imgui.button("Apply Font (Requires Restart)"):
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
app.ai_status = "Font settings saved. Restart required."
|
|
imgui.separator()
|
|
imgui.text("UI Scale (DPI)")
|
|
ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f")
|
|
if ch:
|
|
theme.set_scale(scale)
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
|
|
imgui.text("Panel Transparency")
|
|
ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f")
|
|
if ch_t:
|
|
theme.set_transparency(trans)
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
|
|
imgui.text("Panel Item Transparency")
|
|
ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f")
|
|
if ch_ct:
|
|
theme.set_child_transparency(ctrans)
|
|
bg = bg_shader.get_bg()
|
|
ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled)
|
|
if ch_bg:
|
|
gui_cfg = app.config.setdefault("gui", {})
|
|
gui_cfg["bg_shader_enabled"] = bg.enabled
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
|
|
ch_crt, app.ui_crt_filter = imgui.checkbox("CRT Filter", app.ui_crt_filter)
|
|
if ch_crt:
|
|
gui_cfg = app.config.setdefault("gui", {})
|
|
gui_cfg["crt_filter_enabled"] = app.ui_crt_filter
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
|
|
imgui.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(); models.save_config(app.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(); models.save_config(app.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(); models.save_config(app.config)
|
|
|
|
if imgui.button("Reset Tone Mapping"):
|
|
theme.reset_tone_mapping(curr_p)
|
|
app._flush_to_config()
|
|
models.save_config(app.config)
|
|
|
|
imgui.end()
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel")
|
|
|
|
def render_shader_live_editor(app: App) -> None:
|
|
"""
|
|
[C: tests/test_shader_live_editor.py:test_shader_live_editor_renders]
|
|
"""
|
|
if app.show_windows.get('Shader Editor', False):
|
|
with imscope.window('Shader Editor', app.show_windows['Shader Editor']) as (exp, opened):
|
|
app.show_windows['Shader Editor'] = bool(opened)
|
|
if exp:
|
|
changed_crt, app.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', app.shader_uniforms['crt'], 0.0, 2.0)
|
|
changed_scan, app.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', app.shader_uniforms['scanline'], 0.0, 1.0)
|
|
changed_bloom, app.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', app.shader_uniforms['bloom'], 0.0, 1.0)
|
|
|
|
def render_markdown_test(app: App) -> None:
|
|
imgui.text("Markdown Test Panel")
|
|
imgui.separator()
|
|
md = """
|
|
# Header 1
|
|
## Header 2
|
|
### Header 3
|
|
This is **bold** text and *italic* text.
|
|
And ***bold italic*** text.
|
|
|
|
* List item 1
|
|
* List item 2
|
|
* Sub-item
|
|
|
|
[Link to Google](https://google.com)
|
|
|
|
```python
|
|
|
|
def hello():
|
|
print("Markdown works!")
|
|
```
|
|
"""
|
|
markdown_helper.render(md)
|
|
|
|
def render_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:
|
|
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.
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle]
|
|
"""
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_mma_dashboard")
|
|
render_mma_focus_selector(app)
|
|
imgui.separator()
|
|
if app.is_viewing_prior_session:
|
|
c = 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."""
|
|
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:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
is_nerv = theme.is_nerv_active()
|
|
track_name = app.active_track.description if app.active_track else "None"
|
|
if getattr(app, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph"
|
|
track_stats = project_manager.calculate_track_progress(app.active_track.tickets if app.active_track else app.active_tickets)
|
|
total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in app.mma_tier_usage.values())
|
|
imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL(), track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line()
|
|
if app.mma_status == "paused":
|
|
imgui.text_colored(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:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
imgui.text_colored(C_LBL(), 'Epic Planning (Tier 1)')
|
|
_, app.ui_epic_input = imgui.input_text_multiline('##epic_input', app.ui_epic_input, imgui.ImVec2(-1, 80))
|
|
if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): app._cb_plan_epic()
|
|
|
|
def render_mma_conductor_setup(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
if imgui.button("Run Setup Scan"): app._cb_run_conductor_setup()
|
|
if app.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", app.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only)
|
|
|
|
def render_mma_track_browser(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
imgui.text("Track Browser")
|
|
if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
|
|
imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row()
|
|
for track in app.tracks:
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column()
|
|
status = track.get("status", "unknown").lower()
|
|
c = 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:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
changed, app.mma_step_mode = imgui.checkbox("Step Mode (HITL)", app.mma_step_mode)
|
|
imgui.same_line(); imgui.text(f"Status: {app.mma_status.upper()}")
|
|
if app.controller and hasattr(app.controller, 'engine') and app.controller.engine and hasattr(app.controller.engine, '_pause_event'):
|
|
imgui.same_line()
|
|
is_paused = app.controller.engine._pause_event.is_set()
|
|
if imgui.button("Resume" if is_paused else "Pause"):
|
|
if is_paused: app.controller.engine.resume()
|
|
else: app.controller.engine.pause()
|
|
if app.active_tier:
|
|
imgui.same_line(); imgui.text_colored(C_VAL(), f"| Active: {app.active_tier}")
|
|
any_pending = len(app._pending_mma_spawns) > 0 or len(app._pending_mma_approvals) > 0 or app._pending_ask_dialog
|
|
if any_pending:
|
|
alpha = abs(math.sin(time.time() * 5))
|
|
c = 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:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
imgui.text("Tier Usage (Tokens & Cost)")
|
|
if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
|
|
imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row()
|
|
total_cost = 0.0
|
|
for tier, stats in app.mma_tier_usage.items():
|
|
imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}")
|
|
imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table()
|
|
if imgui.collapsing_header("Tier Model Config"):
|
|
for tier in app.mma_tier_usage.keys():
|
|
imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = app.mma_tier_usage[tier].get("model", "unknown"), app.mma_tier_usage[tier].get("provider", "gemini")
|
|
with imscope.id(f"tier_cfg_{tier}"):
|
|
imgui.push_item_width(80)
|
|
if imgui.begin_combo("##prov", curr_prov):
|
|
for p in models.PROVIDERS:
|
|
if imgui.selectable(p, p == curr_prov)[0]:
|
|
app.mma_tier_usage[tier]["provider"] = p
|
|
models_list = app.controller.all_available_models.get(p, [])
|
|
if models_list: app.mma_tier_usage[tier]["model"] = models_list[0]
|
|
imgui.end_combo()
|
|
imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150)
|
|
models_list = app.controller.all_available_models.get(curr_prov, [])
|
|
if imgui.begin_combo("##model", curr_model):
|
|
for m in models_list:
|
|
if imgui.selectable(m, curr_model == m)[0]: app.mma_tier_usage[tier]["model"] = m
|
|
imgui.end_combo()
|
|
imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1)
|
|
curr_preset = app.mma_tier_usage[tier].get("tool_preset") or "None"
|
|
p_names = ["None"] + sorted(app.controller.tool_presets.keys())
|
|
if imgui.begin_combo("##preset", curr_preset):
|
|
for pn in p_names:
|
|
if imgui.selectable(pn, curr_preset == pn)[0]: app.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn
|
|
imgui.end_combo()
|
|
imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150)
|
|
curr_pers = app.mma_tier_usage[tier].get("persona") or "None"
|
|
personas = getattr(app.controller, 'personas', {})
|
|
pers_opts = ["None"] + sorted(personas.keys())
|
|
if imgui.begin_combo("##persona", curr_pers):
|
|
for pern in pers_opts:
|
|
if imgui.selectable(pern, curr_pers == pern)[0]: app.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern
|
|
imgui.end_combo()
|
|
imgui.pop_item_width()
|
|
|
|
def render_mma_ticket_editor(app: App) -> None:
|
|
imgui.separator(); imgui.text_colored(C_VAL(), f"Editing: {app.ui_selected_ticket_id}")
|
|
ticket = next((t for t in app.active_tickets if str(t.get('id', '')) == app.ui_selected_ticket_id), None)
|
|
if ticket:
|
|
imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium')
|
|
imgui.text("Priority:"); imgui.same_line()
|
|
if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio):
|
|
for p_opt in ['high', 'medium', 'low']:
|
|
if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; app._push_mma_state_update()
|
|
imgui.end_combo()
|
|
imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}")
|
|
personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '')
|
|
imgui.text("Persona Override:"); imgui.same_line()
|
|
pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0
|
|
_, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts)
|
|
ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx]
|
|
if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket['status'] = 'done'; app._push_mma_state_update()
|
|
imgui.same_line()
|
|
if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): app.active_tickets = [t for t in app.active_tickets if str(t.get('id', '')) != app.ui_selected_ticket_id]; app.ui_selected_ticket_id = None; app._push_mma_state_update()
|
|
|
|
def render_mma_agent_streams(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
imgui.text("Agent Streams")
|
|
if imgui.begin_tab_bar("mma_streams_tabs"):
|
|
for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]:
|
|
with imscope.tab_item(tier) as (exp, _):
|
|
if exp:
|
|
sep_val = getattr(app, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val)
|
|
if ch:
|
|
setattr(app, sep_flag_attr, new_val)
|
|
app.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val
|
|
if not new_val: render_tier_stream_panel(app, tier, label)
|
|
else: imgui.text_disabled(f"{tier} stream is detached.")
|
|
if getattr(app, "ui_project_execution_mode", "native") == "beads":
|
|
with imscope.tab_item("Beads") as (exp, _):
|
|
if exp: render_beads_tab(app)
|
|
imgui.end_tab_bar()
|
|
|
|
def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) -> None:
|
|
"""
|
|
[C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders]
|
|
"""
|
|
if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tier_stream_panel")
|
|
if app.is_viewing_prior_session:
|
|
imgui.text_colored(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:
|
|
if len(content) != app._tier_stream_last_len.get(stream_key, -1):
|
|
imgui.set_scroll_here_y(1.0)
|
|
app._tier_stream_last_len[stream_key] = len(content)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
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)
|
|
try:
|
|
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
|
|
imgui.set_scroll_here_y(1.0)
|
|
app._tier_stream_last_len[key] = len(app.mma_streams[key])
|
|
imgui.end_child()
|
|
except (TypeError, AttributeError):
|
|
imgui.end_child()
|
|
pass
|
|
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel")
|
|
|
|
def render_track_proposal_modal(app: App) -> None:
|
|
if app._show_track_proposal_modal:
|
|
imgui.open_popup("Track Proposal")
|
|
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
|
from src import shaders
|
|
p_min = imgui.get_window_pos()
|
|
p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y)
|
|
# Render soft shadow behind the modal
|
|
shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0)
|
|
|
|
if app._show_track_proposal_modal:
|
|
imgui.text_colored(C_IN(), "Proposed Implementation Tracks")
|
|
imgui.separator()
|
|
if not app.proposed_tracks:
|
|
imgui.text("No tracks generated.")
|
|
else:
|
|
for idx, track in enumerate(app.proposed_tracks):
|
|
# Title Edit
|
|
changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', ''))
|
|
if changed_t:
|
|
track['title'] = new_t
|
|
# Goal Edit
|
|
changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60))
|
|
if changed_g:
|
|
track['goal'] = new_g
|
|
# Buttons
|
|
if imgui.button(f"Remove##{idx}"):
|
|
app.proposed_tracks.pop(idx)
|
|
break
|
|
imgui.same_line()
|
|
if imgui.button(f"Start This Track##{idx}"):
|
|
app._cb_start_track(idx)
|
|
imgui.separator()
|
|
if imgui.button("Accept", imgui.ImVec2(120, 0)):
|
|
app._cb_accept_tracks()
|
|
app._show_track_proposal_modal = False
|
|
imgui.close_current_popup()
|
|
imgui.same_line()
|
|
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
|
app._show_track_proposal_modal = False
|
|
imgui.close_current_popup()
|
|
else:
|
|
imgui.close_current_popup()
|
|
imgui.end_popup()
|
|
|
|
def render_ticket_queue(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns]
|
|
"""
|
|
imgui.text("Ticket Queue Management")
|
|
if not app.active_track:
|
|
imgui.text_disabled("No active track.")
|
|
return
|
|
|
|
# Select All / None
|
|
if imgui.button("Select All"): app.ui_selected_tickets = {str(t.get('id', '')) for t in app.active_tickets}
|
|
imgui.same_line()
|
|
if imgui.button("Select None"): app.ui_selected_tickets.clear()
|
|
|
|
imgui.same_line(); imgui.spacing(); imgui.same_line()
|
|
|
|
# Bulk Actions
|
|
if imgui.button("Bulk Execute"): app.bulk_execute()
|
|
imgui.same_line()
|
|
if imgui.button("Bulk Skip"): app.bulk_skip()
|
|
imgui.same_line()
|
|
if imgui.button("Bulk Block"): app.bulk_block()
|
|
# Table
|
|
flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y
|
|
if imgui.begin_table("ticket_queue_table", 7, flags, imgui.ImVec2(0, 300)):
|
|
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
|
|
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
|
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
|
|
imgui.table_setup_column("Model", imgui.TableColumnFlags_.width_fixed, 150)
|
|
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
|
imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch)
|
|
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80)
|
|
imgui.table_headers_row()
|
|
|
|
for i, t in enumerate(app.active_tickets):
|
|
tid = str(t.get('id', ''))
|
|
imgui.table_next_row()
|
|
|
|
# Select
|
|
imgui.table_next_column()
|
|
is_sel = tid in app.ui_selected_tickets
|
|
changed, is_sel = imgui.checkbox(f"##sel_{tid}", is_sel)
|
|
if changed:
|
|
if is_sel: app.ui_selected_tickets.add(tid)
|
|
else: app.ui_selected_tickets.discard(tid)
|
|
|
|
# ID
|
|
imgui.table_next_column()
|
|
is_selected = (tid == app.ui_selected_ticket_id)
|
|
opened, _ = imgui.selectable(f"{tid}##drag_{tid}", is_selected)
|
|
if opened: app.ui_selected_ticket_id = tid
|
|
|
|
if imgui.begin_drag_drop_source():
|
|
imgui.set_drag_drop_payload("TICKET_REORDER", i)
|
|
imgui.text(f"Moving {tid}")
|
|
imgui.end_drag_drop_source()
|
|
|
|
if imgui.begin_drag_drop_target():
|
|
payload = imgui.accept_drag_drop_payload("TICKET_REORDER")
|
|
if payload:
|
|
src_idx = int(payload.data)
|
|
app._reorder_ticket(src_idx, i)
|
|
imgui.end_drag_drop_target()
|
|
|
|
# Priority
|
|
|
|
imgui.table_next_column()
|
|
prio = t.get('priority', 'medium')
|
|
p_col = 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
|
|
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
|
|
try:
|
|
from src.dag_engine import TrackDAG
|
|
ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in app.active_tickets]
|
|
temp_dag = TrackDAG(ticket_dicts)
|
|
if temp_dag.has_cycle():
|
|
imgui.open_popup("Cycle Detected!")
|
|
except Exception:
|
|
pass
|
|
ed.end()
|
|
# 5. Add Ticket Form
|
|
imgui.separator()
|
|
if imgui.button("Add Ticket"):
|
|
app._show_add_ticket_form = not app._show_add_ticket_form
|
|
if app._show_add_ticket_form:
|
|
# Default Ticket ID
|
|
max_id = 0
|
|
for t in app.active_tickets:
|
|
tid = t.get('id', '')
|
|
if tid.startswith('T-'):
|
|
try: max_id = max(max_id, int(tid[2:]))
|
|
except: pass
|
|
app.ui_new_ticket_id = f"T-{max_id + 1:03d}"
|
|
app.ui_new_ticket_desc = ""
|
|
app.ui_new_ticket_target = ""
|
|
app.ui_new_ticket_deps = ""
|
|
if app._show_add_ticket_form:
|
|
imgui.begin_child("add_ticket_form", imgui.ImVec2(-1, 220), True)
|
|
imgui.text_colored(C_VAL(), "New Ticket Details")
|
|
_, app.ui_new_ticket_id = imgui.input_text("ID##new_ticket", app.ui_new_ticket_id)
|
|
_, app.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", app.ui_new_ticket_desc, imgui.ImVec2(-1, 60))
|
|
_, app.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", app.ui_new_ticket_target)
|
|
_, app.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", app.ui_new_ticket_deps)
|
|
imgui.text("Priority:")
|
|
imgui.same_line()
|
|
if imgui.begin_combo("##new_prio", app.ui_new_ticket_priority):
|
|
for p_opt in ['high', 'medium', 'low']:
|
|
if imgui.selectable(p_opt, p_opt == app.ui_new_ticket_priority)[0]:
|
|
app.ui_new_ticket_priority = p_opt
|
|
imgui.end_combo()
|
|
if imgui.button("Create"):
|
|
new_ticket = {
|
|
"id": app.ui_new_ticket_id,
|
|
"description": app.ui_new_ticket_desc,
|
|
"status": "todo",
|
|
"priority": app.ui_new_ticket_priority,
|
|
"assigned_to": "tier3-worker",
|
|
"target_file": app.ui_new_ticket_target,
|
|
"depends_on": [d.strip() for d in app.ui_new_ticket_deps.split(",") if d.strip()]
|
|
}
|
|
app.active_tickets.append(new_ticket)
|
|
app._show_add_ticket_form = False
|
|
app._push_mma_state_update()
|
|
imgui.same_line()
|
|
if imgui.button("Cancel"): app._show_add_ticket_form = False
|
|
imgui.end_child()
|
|
else:
|
|
imgui.text_disabled("No active MMA track or tickets.")
|
|
|
|
def render_beads_tab(app: App) -> None:
|
|
imgui.text("Beads Graph (Dolt-backed)")
|
|
if imgui.button("Refresh Beads"):
|
|
pass
|
|
imgui.separator()
|
|
|
|
# Check for dolt/bd dependencies
|
|
dolt_path = shutil.which("dolt")
|
|
bd_path = shutil.which("bd")
|
|
if not dolt_path or not bd_path:
|
|
missing = []
|
|
if not dolt_path: missing.append("'dolt'")
|
|
if not bd_path: missing.append("'bd'")
|
|
imgui.text_colored(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":
|
|
try:
|
|
from src import beads_client
|
|
bclient = beads_client.BeadsClient(Path(app.active_project_root))
|
|
beads = bclient.list_beads()
|
|
if not beads:
|
|
imgui.text_disabled("No beads found.")
|
|
else:
|
|
if imgui.begin_table("beads_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
|
|
imgui.table_setup_column("ID")
|
|
imgui.table_setup_column("Status")
|
|
imgui.table_setup_column("Title")
|
|
imgui.table_headers_row()
|
|
for b in beads:
|
|
imgui.table_next_row()
|
|
imgui.table_next_column()
|
|
imgui.text(str(b.id))
|
|
imgui.table_next_column()
|
|
imgui.text(str(b.status))
|
|
imgui.table_next_column()
|
|
imgui.text(str(b.title))
|
|
imgui.end_table()
|
|
except Exception as e:
|
|
imgui.text_colored(theme.get_color("status_error"), f"Error loading beads: {e}")
|
|
|
|
def render_mma_focus_selector(app: App) -> None:
|
|
"""
|
|
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress]
|
|
"""
|
|
imgui.text("Focus Agent:"); imgui.same_line()
|
|
focus_label = app.ui_focus_agent or "All"
|
|
if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview):
|
|
if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None
|
|
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
|
|
if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier
|
|
imgui.end_combo()
|
|
imgui.same_line()
|
|
if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None
|
|
|
|
#endregion: MMA
|