Port missing features to gui_2 and optimize caching
- Port 10 missing features from gui.py to gui_2.py: performance
diagnostics, prior session log viewing, token budget visualization,
agent tools config, API hooks server, GUI task queue, discussion
truncation, THINKING/LIVE indicators, event subscriptions, and
session usage tracking
- Persist window visibility state in config.toml
- Fix Gemini cache invalidation by separating discussion history
from cached context (use MD5 hash instead of built-in hash)
- Add cost optimizations: tool output truncation at source, proactive
history trimming at 40%, summary_only support in aggregate.run()
- Add cleanup() for destroying API caches on exit
This commit is contained in:
521
gui_2.py
521
gui_2.py
@@ -4,6 +4,8 @@ import threading
|
||||
import time
|
||||
import math
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
import aggregate
|
||||
@@ -14,6 +16,9 @@ import session_logger
|
||||
import project_manager
|
||||
import theme_2 as theme
|
||||
import tomllib
|
||||
import numpy as np
|
||||
import api_hooks
|
||||
from performance_monitor import PerformanceMonitor
|
||||
|
||||
from imgui_bundle import imgui, hello_imgui, immapp
|
||||
|
||||
@@ -56,6 +61,15 @@ KIND_COLORS = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_res
|
||||
HEAVY_KEYS = {"message", "text", "script", "output", "content"}
|
||||
|
||||
DISC_ROLES = ["User", "AI", "Vendor API", "System"]
|
||||
AGENT_TOOL_NAMES = ["run_powershell", "read_file", "list_directory", "search_files", "get_file_summary", "web_search", "fetch_url"]
|
||||
|
||||
def truncate_entries(entries: list[dict], max_pairs: int) -> list[dict]:
|
||||
if max_pairs <= 0:
|
||||
return []
|
||||
target_count = max_pairs * 2
|
||||
if len(entries) <= target_count:
|
||||
return entries
|
||||
return entries[-target_count:]
|
||||
|
||||
def _parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict]:
|
||||
known = roles if roles is not None else DISC_ROLES
|
||||
@@ -119,6 +133,7 @@ class App:
|
||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
||||
self.ui_word_wrap = proj_meta.get("word_wrap", True)
|
||||
self.ui_summary_only = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||
|
||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||
@@ -139,7 +154,7 @@ class App:
|
||||
self.send_thread: threading.Thread | None = None
|
||||
self.models_thread: threading.Thread | None = None
|
||||
|
||||
self.show_windows = {
|
||||
_default_windows = {
|
||||
"Projects": True,
|
||||
"Files": True,
|
||||
"Screenshots": True,
|
||||
@@ -151,7 +166,10 @@ class App:
|
||||
"Comms History": True,
|
||||
"System Prompts": True,
|
||||
"Theme": True,
|
||||
"Diagnostics": False,
|
||||
}
|
||||
saved = self.config.get("gui", {}).get("show_windows", {})
|
||||
self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
|
||||
self.show_script_output = False
|
||||
self.show_text_viewer = False
|
||||
self.text_viewer_title = ""
|
||||
@@ -181,12 +199,49 @@ class App:
|
||||
|
||||
self._scroll_disc_to_bottom = False
|
||||
|
||||
# GUI Task Queue (thread-safe, for event handlers and hook server)
|
||||
self._pending_gui_tasks: list[dict] = []
|
||||
self._pending_gui_tasks_lock = threading.Lock()
|
||||
|
||||
# Session usage tracking
|
||||
self.session_usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}
|
||||
|
||||
# Token budget / cache telemetry
|
||||
self._token_budget_pct = 0.0
|
||||
self._token_budget_current = 0
|
||||
self._token_budget_limit = 0
|
||||
self._gemini_cache_text = ""
|
||||
|
||||
# Discussion truncation
|
||||
self.ui_disc_truncate_pairs: int = 2
|
||||
|
||||
# Agent tools config
|
||||
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools: dict[str, bool] = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
|
||||
# Prior session log viewing
|
||||
self.is_viewing_prior_session = False
|
||||
self.prior_session_entries: list[dict] = []
|
||||
|
||||
# API Hooks
|
||||
self.test_hooks_enabled = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||
|
||||
# Performance monitoring
|
||||
self.perf_monitor = PerformanceMonitor()
|
||||
self.perf_history = {"frame_time": [0.0]*100, "fps": [0.0]*100, "cpu": [0.0]*100, "input_lag": [0.0]*100}
|
||||
self._perf_last_update = 0.0
|
||||
|
||||
session_logger.open_session()
|
||||
ai_client.set_provider(self.current_provider, self.current_model)
|
||||
ai_client.confirm_and_run_callback = self._confirm_and_run
|
||||
ai_client.comms_log_callback = self._on_comms_entry
|
||||
ai_client.tool_log_callback = self._on_tool_log
|
||||
|
||||
# AI client event subscriptions
|
||||
ai_client.events.on("request_start", self._on_api_event)
|
||||
ai_client.events.on("response_received", self._on_api_event)
|
||||
ai_client.events.on("tool_execution", self._on_api_event)
|
||||
|
||||
# ---------------------------------------------------------------- project loading
|
||||
|
||||
def _load_active_project(self):
|
||||
@@ -253,6 +308,10 @@ class App:
|
||||
self.ui_project_main_context = proj.get("project", {}).get("main_context", "")
|
||||
self.ui_auto_add_history = proj.get("discussion", {}).get("auto_add", False)
|
||||
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
|
||||
self.ui_summary_only = proj.get("project", {}).get("summary_only", False)
|
||||
|
||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
|
||||
def _save_active_project(self):
|
||||
if self.active_project_path:
|
||||
@@ -337,6 +396,76 @@ class App:
|
||||
def _on_tool_log(self, script: str, result: str):
|
||||
session_logger.log_tool_call(script, result, None)
|
||||
|
||||
def _on_api_event(self, *args, **kwargs):
|
||||
payload = kwargs.get("payload", {})
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({"action": "refresh_api_metrics", "payload": payload})
|
||||
|
||||
def _process_pending_gui_tasks(self):
|
||||
if not self._pending_gui_tasks:
|
||||
return
|
||||
with self._pending_gui_tasks_lock:
|
||||
tasks = self._pending_gui_tasks[:]
|
||||
self._pending_gui_tasks.clear()
|
||||
for task in tasks:
|
||||
try:
|
||||
action = task.get("action")
|
||||
if action == "refresh_api_metrics":
|
||||
self._refresh_api_metrics(task.get("payload", {}))
|
||||
except Exception as e:
|
||||
print(f"Error executing GUI task: {e}")
|
||||
|
||||
def _recalculate_session_usage(self):
|
||||
usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}
|
||||
for entry in ai_client.get_comms_log():
|
||||
if entry.get("kind") == "response" and "usage" in entry.get("payload", {}):
|
||||
u = entry["payload"]["usage"]
|
||||
for k in usage.keys():
|
||||
usage[k] += u.get(k, 0) or 0
|
||||
self.session_usage = usage
|
||||
|
||||
def _refresh_api_metrics(self, payload: dict):
|
||||
self._recalculate_session_usage()
|
||||
try:
|
||||
stats = ai_client.get_history_bleed_stats()
|
||||
self._token_budget_pct = stats.get("percentage", 0.0) / 100.0
|
||||
self._token_budget_current = stats.get("current", 0)
|
||||
self._token_budget_limit = stats.get("limit", 0)
|
||||
except Exception:
|
||||
pass
|
||||
cache_stats = payload.get("cache_stats")
|
||||
if cache_stats:
|
||||
count = cache_stats.get("cache_count", 0)
|
||||
size_bytes = cache_stats.get("total_size_bytes", 0)
|
||||
self._gemini_cache_text = f"Gemini Caches: {count} ({size_bytes / 1024:.1f} KB)"
|
||||
|
||||
def cb_load_prior_log(self):
|
||||
root = hide_tk_root()
|
||||
path = filedialog.askopenfilename(
|
||||
title="Load Session Log",
|
||||
initialdir="logs",
|
||||
filetypes=[("Log/JSONL", "*.log *.jsonl"), ("All Files", "*.*")]
|
||||
)
|
||||
root.destroy()
|
||||
if not path:
|
||||
return
|
||||
entries = []
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.ai_status = f"log load error: {e}"
|
||||
return
|
||||
self.prior_session_entries = entries
|
||||
self.is_viewing_prior_session = True
|
||||
self.ai_status = f"viewing prior session: {Path(path).name} ({len(entries)} entries)"
|
||||
|
||||
def _confirm_and_run(self, script: str, base_dir: str) -> str | None:
|
||||
dialog = ConfirmDialog(script, base_dir)
|
||||
with self._pending_dialog_lock:
|
||||
@@ -373,6 +502,11 @@ class App:
|
||||
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
||||
proj["project"]["main_context"] = self.ui_project_main_context
|
||||
proj["project"]["word_wrap"] = self.ui_word_wrap
|
||||
proj["project"]["summary_only"] = self.ui_summary_only
|
||||
|
||||
proj.setdefault("agent", {}).setdefault("tools", {})
|
||||
for t_name in AGENT_TOOL_NAMES:
|
||||
proj["agent"]["tools"][t_name] = self.ui_agent_tools.get(t_name, True)
|
||||
|
||||
self._flush_disc_entries_to_project()
|
||||
disc_sec = proj.setdefault("discussion", {})
|
||||
@@ -390,15 +524,26 @@ class App:
|
||||
}
|
||||
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
||||
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
|
||||
self.config["gui"] = {"show_windows": self.show_windows}
|
||||
theme.save_to_config(self.config)
|
||||
|
||||
def _do_generate(self) -> tuple[str, Path, list]:
|
||||
def _do_generate(self) -> tuple[str, Path, list, str, str]:
|
||||
"""Returns (full_md, output_path, file_items, stable_md, discussion_text)."""
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
self._flush_to_config()
|
||||
save_config(self.config)
|
||||
flat = project_manager.flat_config(self.project, self.active_discussion)
|
||||
return aggregate.run(flat)
|
||||
full_md, path, file_items = aggregate.run(flat)
|
||||
# Build stable markdown (no history) for Gemini caching
|
||||
screenshot_base_dir = Path(flat.get("screenshots", {}).get("base_dir", "."))
|
||||
screenshots = flat.get("screenshots", {}).get("paths", [])
|
||||
summary_only = flat.get("project", {}).get("summary_only", False)
|
||||
stable_md = aggregate.build_markdown_no_history(file_items, screenshot_base_dir, screenshots, summary_only=summary_only)
|
||||
# Build discussion history text separately
|
||||
history = flat.get("discussion", {}).get("history", [])
|
||||
discussion_text = aggregate.build_discussion_text(history)
|
||||
return full_md, path, file_items, stable_md, discussion_text
|
||||
|
||||
def _fetch_models(self, provider: str):
|
||||
self.ai_status = "fetching models..."
|
||||
@@ -445,6 +590,11 @@ class App:
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
def _gui_func(self):
|
||||
self.perf_monitor.start_frame()
|
||||
|
||||
# Process GUI task queue
|
||||
self._process_pending_gui_tasks()
|
||||
|
||||
# Sync pending comms
|
||||
with self._pending_comms_lock:
|
||||
for c in self._pending_comms:
|
||||
@@ -576,6 +726,14 @@ class App:
|
||||
self.ai_status = "config saved"
|
||||
|
||||
ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap)
|
||||
ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only)
|
||||
|
||||
if imgui.collapsing_header("Agent Tools"):
|
||||
for t_name in AGENT_TOOL_NAMES:
|
||||
val = self.ui_agent_tools.get(t_name, True)
|
||||
ch, val = imgui.checkbox(f"Enable {t_name}", val)
|
||||
if ch:
|
||||
self.ui_agent_tools[t_name] = val
|
||||
imgui.end()
|
||||
|
||||
# ---- Files
|
||||
@@ -655,7 +813,50 @@ class App:
|
||||
if self.show_windows["Discussion History"]:
|
||||
exp, self.show_windows["Discussion History"] = imgui.begin("Discussion History", self.show_windows["Discussion History"])
|
||||
if exp:
|
||||
if imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
||||
# THINKING indicator
|
||||
is_thinking = self.ai_status in ["sending..."]
|
||||
if is_thinking:
|
||||
val = math.sin(time.time() * 10 * math.pi)
|
||||
alpha = 1.0 if val > 0 else 0.0
|
||||
imgui.text_colored(imgui.ImVec4(1.0, 0.39, 0.39, alpha), "THINKING...")
|
||||
imgui.separator()
|
||||
|
||||
# Prior session viewing mode
|
||||
if self.is_viewing_prior_session:
|
||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
||||
imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION")
|
||||
imgui.same_line()
|
||||
if imgui.button("Exit Prior Session"):
|
||||
self.is_viewing_prior_session = False
|
||||
self.prior_session_entries.clear()
|
||||
imgui.separator()
|
||||
imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False)
|
||||
for idx, entry in enumerate(self.prior_session_entries):
|
||||
imgui.push_id(f"prior_{idx}")
|
||||
kind = entry.get("kind", entry.get("type", ""))
|
||||
imgui.text_colored(C_LBL, f"#{idx+1}")
|
||||
imgui.same_line()
|
||||
ts = entry.get("ts", entry.get("timestamp", ""))
|
||||
if ts:
|
||||
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_KEY, str(kind))
|
||||
payload = entry.get("payload", entry)
|
||||
text = payload.get("text", payload.get("message", payload.get("content", "")))
|
||||
if text:
|
||||
preview = str(text).replace("\n", " ")[:200]
|
||||
if self.ui_word_wrap:
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(preview)
|
||||
imgui.pop_text_wrap_pos()
|
||||
else:
|
||||
imgui.text(preview)
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
imgui.end_child()
|
||||
imgui.pop_style_color()
|
||||
|
||||
if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
||||
names = self._get_discussion_names()
|
||||
|
||||
if imgui.begin_combo("##disc_sel", self.active_discussion):
|
||||
@@ -702,106 +903,121 @@ class App:
|
||||
if imgui.button("Delete"):
|
||||
self._delete_discussion(self.active_discussion)
|
||||
|
||||
imgui.separator()
|
||||
if imgui.button("+ Entry"):
|
||||
self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
if imgui.button("-All"):
|
||||
for e in self.disc_entries: e["collapsed"] = True
|
||||
imgui.same_line()
|
||||
if imgui.button("+All"):
|
||||
for e in self.disc_entries: e["collapsed"] = False
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear All"):
|
||||
self.disc_entries.clear()
|
||||
imgui.same_line()
|
||||
if imgui.button("Save"):
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
self._flush_to_config()
|
||||
save_config(self.config)
|
||||
self.ai_status = "discussion saved"
|
||||
|
||||
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
||||
imgui.separator()
|
||||
|
||||
if imgui.collapsing_header("Roles"):
|
||||
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
||||
for i, r in enumerate(self.disc_roles):
|
||||
if imgui.button(f"x##r{i}"):
|
||||
self.disc_roles.pop(i)
|
||||
break
|
||||
imgui.same_line()
|
||||
imgui.text(r)
|
||||
imgui.end_child()
|
||||
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
||||
imgui.same_line()
|
||||
if imgui.button("Add"):
|
||||
r = self.ui_disc_new_role_input.strip()
|
||||
if r and r not in self.disc_roles:
|
||||
self.disc_roles.append(r)
|
||||
self.ui_disc_new_role_input = ""
|
||||
|
||||
imgui.separator()
|
||||
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
||||
for i, entry in enumerate(self.disc_entries):
|
||||
imgui.push_id(str(i))
|
||||
collapsed = entry.get("collapsed", False)
|
||||
read_mode = entry.get("read_mode", False)
|
||||
|
||||
if imgui.button("+" if collapsed else "-"):
|
||||
entry["collapsed"] = not collapsed
|
||||
imgui.same_line()
|
||||
|
||||
imgui.set_next_item_width(120)
|
||||
if imgui.begin_combo("##role", entry["role"]):
|
||||
for r in self.disc_roles:
|
||||
if imgui.selectable(r, r == entry["role"])[0]:
|
||||
entry["role"] = r
|
||||
imgui.end_combo()
|
||||
|
||||
if not collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
||||
entry["read_mode"] = not read_mode
|
||||
|
||||
ts_str = entry.get("ts", "")
|
||||
if ts_str:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(vec4(120, 120, 100), ts_str)
|
||||
|
||||
if collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("Ins"):
|
||||
self.disc_entries.insert(i, {"role": "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
self._render_text_viewer(f"Entry #{i+1}", entry["content"])
|
||||
imgui.same_line()
|
||||
if imgui.button("Del"):
|
||||
self.disc_entries.pop(i)
|
||||
imgui.pop_id()
|
||||
break
|
||||
imgui.same_line()
|
||||
preview = entry["content"].replace("\n", " ")[:60]
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||
|
||||
if not collapsed:
|
||||
if read_mode:
|
||||
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(entry["content"])
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
|
||||
if not self.is_viewing_prior_session:
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
if self._scroll_disc_to_bottom:
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
self._scroll_disc_to_bottom = False
|
||||
imgui.end_child()
|
||||
if imgui.button("+ Entry"):
|
||||
self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
if imgui.button("-All"):
|
||||
for e in self.disc_entries: e["collapsed"] = True
|
||||
imgui.same_line()
|
||||
if imgui.button("+All"):
|
||||
for e in self.disc_entries: e["collapsed"] = False
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear All"):
|
||||
self.disc_entries.clear()
|
||||
imgui.same_line()
|
||||
if imgui.button("Save"):
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
self._flush_to_config()
|
||||
save_config(self.config)
|
||||
self.ai_status = "discussion saved"
|
||||
imgui.same_line()
|
||||
if imgui.button("Load Log"):
|
||||
self.cb_load_prior_log()
|
||||
|
||||
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
||||
|
||||
# Truncation controls
|
||||
imgui.text("Keep Pairs:")
|
||||
imgui.same_line()
|
||||
imgui.set_next_item_width(80)
|
||||
ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1)
|
||||
if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1
|
||||
imgui.same_line()
|
||||
if imgui.button("Truncate"):
|
||||
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
|
||||
self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs"
|
||||
imgui.separator()
|
||||
|
||||
if imgui.collapsing_header("Roles"):
|
||||
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
||||
for i, r in enumerate(self.disc_roles):
|
||||
if imgui.button(f"x##r{i}"):
|
||||
self.disc_roles.pop(i)
|
||||
break
|
||||
imgui.same_line()
|
||||
imgui.text(r)
|
||||
imgui.end_child()
|
||||
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
||||
imgui.same_line()
|
||||
if imgui.button("Add"):
|
||||
r = self.ui_disc_new_role_input.strip()
|
||||
if r and r not in self.disc_roles:
|
||||
self.disc_roles.append(r)
|
||||
self.ui_disc_new_role_input = ""
|
||||
|
||||
imgui.separator()
|
||||
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
||||
for i, entry in enumerate(self.disc_entries):
|
||||
imgui.push_id(str(i))
|
||||
collapsed = entry.get("collapsed", False)
|
||||
read_mode = entry.get("read_mode", False)
|
||||
|
||||
if imgui.button("+" if collapsed else "-"):
|
||||
entry["collapsed"] = not collapsed
|
||||
imgui.same_line()
|
||||
|
||||
imgui.set_next_item_width(120)
|
||||
if imgui.begin_combo("##role", entry["role"]):
|
||||
for r in self.disc_roles:
|
||||
if imgui.selectable(r, r == entry["role"])[0]:
|
||||
entry["role"] = r
|
||||
imgui.end_combo()
|
||||
|
||||
if not collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
||||
entry["read_mode"] = not read_mode
|
||||
|
||||
ts_str = entry.get("ts", "")
|
||||
if ts_str:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(vec4(120, 120, 100), ts_str)
|
||||
|
||||
if collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("Ins"):
|
||||
self.disc_entries.insert(i, {"role": "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
self._render_text_viewer(f"Entry #{i+1}", entry["content"])
|
||||
imgui.same_line()
|
||||
if imgui.button("Del"):
|
||||
self.disc_entries.pop(i)
|
||||
imgui.pop_id()
|
||||
break
|
||||
imgui.same_line()
|
||||
preview = entry["content"].replace("\n", " ")[:60]
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||
|
||||
if not collapsed:
|
||||
if read_mode:
|
||||
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(entry["content"])
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
if self._scroll_disc_to_bottom:
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
self._scroll_disc_to_bottom = False
|
||||
imgui.end_child()
|
||||
imgui.end()
|
||||
|
||||
# ---- Provider
|
||||
@@ -836,12 +1052,32 @@ class App:
|
||||
ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f")
|
||||
ch, self.max_tokens = imgui.input_int("Max Tokens (Output)", self.max_tokens, 1024)
|
||||
ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024)
|
||||
|
||||
imgui.separator()
|
||||
imgui.text("Telemetry")
|
||||
usage = self.session_usage
|
||||
total = usage["input_tokens"] + usage["output_tokens"]
|
||||
imgui.text_colored(C_RES, f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})")
|
||||
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']:,}")
|
||||
imgui.text("Token Budget:")
|
||||
imgui.progress_bar(self._token_budget_pct, imgui.ImVec2(-1, 0), f"{self._token_budget_current:,} / {self._token_budget_limit:,}")
|
||||
if self._gemini_cache_text:
|
||||
imgui.text_colored(C_SUB, self._gemini_cache_text)
|
||||
imgui.end()
|
||||
|
||||
# ---- Message
|
||||
if self.show_windows["Message"]:
|
||||
exp, self.show_windows["Message"] = imgui.begin("Message", self.show_windows["Message"])
|
||||
if exp:
|
||||
# LIVE indicator
|
||||
is_live = self.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."]
|
||||
if is_live:
|
||||
val = math.sin(time.time() * 10 * math.pi)
|
||||
alpha = 1.0 if val > 0 else 0.0
|
||||
imgui.text_colored(imgui.ImVec4(0.39, 1.0, 0.39, alpha), "LIVE")
|
||||
imgui.separator()
|
||||
|
||||
ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40))
|
||||
imgui.separator()
|
||||
if imgui.button("Gen + Send"):
|
||||
@@ -859,14 +1095,20 @@ class App:
|
||||
base_dir = self.ui_files_base_dir
|
||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
ai_client.set_agent_tools(self.ui_agent_tools)
|
||||
# For Gemini: send stable_md (no history) as cached context,
|
||||
# and disc_text separately as conversation history.
|
||||
# For Anthropic: send full md (with history) as before.
|
||||
send_md = stable_md # No history in cached context for either provider
|
||||
send_disc = disc_text
|
||||
|
||||
def do_send():
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()})
|
||||
try:
|
||||
resp = ai_client.send(self.last_md, user_msg, base_dir, self.last_file_items)
|
||||
resp = ai_client.send(send_md, user_msg, base_dir, self.last_file_items, send_disc)
|
||||
self.ai_response = resp
|
||||
self.ai_status = "done"
|
||||
self._trigger_blink = True
|
||||
@@ -1168,6 +1410,67 @@ class App:
|
||||
if ch: theme.set_scale(scale)
|
||||
imgui.end()
|
||||
|
||||
# ---- Diagnostics
|
||||
if self.show_windows["Diagnostics"]:
|
||||
exp, self.show_windows["Diagnostics"] = imgui.begin("Diagnostics", self.show_windows["Diagnostics"])
|
||||
if exp:
|
||||
now = time.time()
|
||||
if now - self._perf_last_update >= 0.5:
|
||||
self._perf_last_update = now
|
||||
metrics = self.perf_monitor.get_metrics()
|
||||
self.perf_history["frame_time"].pop(0)
|
||||
self.perf_history["frame_time"].append(metrics.get("last_frame_time_ms", 0.0))
|
||||
self.perf_history["fps"].pop(0)
|
||||
self.perf_history["fps"].append(metrics.get("fps", 0.0))
|
||||
self.perf_history["cpu"].pop(0)
|
||||
self.perf_history["cpu"].append(metrics.get("cpu_percent", 0.0))
|
||||
self.perf_history["input_lag"].pop(0)
|
||||
self.perf_history["input_lag"].append(metrics.get("input_lag_ms", 0.0))
|
||||
|
||||
metrics = self.perf_monitor.get_metrics()
|
||||
imgui.text("Performance Telemetry")
|
||||
imgui.separator()
|
||||
|
||||
if imgui.begin_table("perf_table", 2, imgui.TableFlags_.borders_inner_h):
|
||||
imgui.table_setup_column("Metric")
|
||||
imgui.table_setup_column("Value")
|
||||
imgui.table_headers_row()
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text("FPS")
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{metrics.get('fps', 0.0):.1f}")
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text("Frame Time (ms)")
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{metrics.get('last_frame_time_ms', 0.0):.2f}")
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text("CPU %")
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{metrics.get('cpu_percent', 0.0):.1f}")
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text("Input Lag (ms)")
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{metrics.get('input_lag_ms', 0.0):.1f}")
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
imgui.separator()
|
||||
imgui.text("Frame Time (ms)")
|
||||
imgui.plot_lines("##ft_plot", np.array(self.perf_history["frame_time"], dtype=np.float32), overlay_text="frame_time", graph_size=imgui.ImVec2(-1, 60))
|
||||
imgui.text("CPU %")
|
||||
imgui.plot_lines("##cpu_plot", np.array(self.perf_history["cpu"], dtype=np.float32), overlay_text="cpu", graph_size=imgui.ImVec2(-1, 60))
|
||||
imgui.end()
|
||||
|
||||
self.perf_monitor.end_frame()
|
||||
|
||||
# ---- Modals / Popups
|
||||
with self._pending_dialog_lock:
|
||||
dlg = self._pending_dialog
|
||||
@@ -1293,10 +1596,16 @@ class App:
|
||||
self.runner_params.callbacks.post_init = self._post_init
|
||||
|
||||
self._fetch_models(self.current_provider)
|
||||
|
||||
|
||||
# Start API hooks server (if enabled)
|
||||
self.hook_server = api_hooks.HookServer(self)
|
||||
self.hook_server.start()
|
||||
|
||||
immapp.run(self.runner_params)
|
||||
|
||||
# On exit
|
||||
self.hook_server.stop()
|
||||
self.perf_monitor.stop()
|
||||
ai_client.cleanup() # Destroy active API caches to stop billing
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
|
||||
Reference in New Issue
Block a user