From 69401365be0830b147d0ac77e38d4e728c4ad63c Mon Sep 17 00:00:00 2001 From: razor950 Date: Mon, 23 Feb 2026 20:06:13 -0500 Subject: [PATCH] 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 --- aggregate.py | 15 +- ai_client.py | 110 ++++++--- config.toml | 14 ++ gui_2.py | 521 +++++++++++++++++++++++++++++++++--------- manual_slop.toml | 12 +- manualslop_layout.ini | 36 +-- 6 files changed, 556 insertions(+), 152 deletions(-) diff --git a/aggregate.py b/aggregate.py index 46a8f16..a624ea6 100644 --- a/aggregate.py +++ b/aggregate.py @@ -164,6 +164,18 @@ def build_markdown_from_items(file_items: list[dict], screenshot_base_dir: Path, return "\n\n---\n\n".join(parts) +def build_markdown_no_history(file_items: list[dict], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False) -> str: + """Build markdown with only files + screenshots (no history). Used for stable caching.""" + return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only) + + +def build_discussion_text(history: list[str]) -> str: + """Build just the discussion history section text. Returns empty string if no history.""" + if not history: + return "" + return "## Discussion History\n\n" + build_discussion_section(history) + + def build_markdown(base_dir: Path, files: list[str], screenshot_base_dir: Path, screenshots: list[str], history: list[str], summary_only: bool = False) -> str: parts = [] # STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits @@ -195,8 +207,9 @@ def run(config: dict) -> tuple[str, Path, list[dict]]: output_file = output_dir / f"{namespace}_{increment:03d}.md" # Build file items once, then construct markdown from them (avoids double I/O) file_items = build_file_items(base_dir, files) + summary_only = config.get("project", {}).get("summary_only", False) markdown = build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, - summary_only=False) + summary_only=summary_only) output_file.write_text(markdown, encoding="utf-8") return markdown, output_file, file_items diff --git a/ai_client.py b/ai_client.py index 3a1a168..3b69f1a 100644 --- a/ai_client.py +++ b/ai_client.py @@ -15,6 +15,8 @@ import tomllib import json import time import datetime +import hashlib +import difflib from pathlib import Path import file_cache import mcp_client @@ -435,6 +437,13 @@ def _run_script(script: str, base_dir: str) -> str: return output +def _truncate_tool_output(output: str) -> str: + """Truncate tool output to _history_trunc_limit chars before sending to API.""" + if _history_trunc_limit > 0 and len(output) > _history_trunc_limit: + return output[:_history_trunc_limit] + "\n\n... [TRUNCATED BY SYSTEM TO SAVE TOKENS.]" + return output + + # ------------------------------------------------------------------ dynamic file context refresh def _reread_file_items(file_items: list[dict]) -> tuple[list[dict], list[dict]]: @@ -460,7 +469,7 @@ def _reread_file_items(file_items: list[dict]) -> tuple[list[dict], list[dict]]: refreshed.append(item) # unchanged — skip re-read continue content = p.read_text(encoding="utf-8") - new_item = {**item, "content": content, "error": False, "mtime": current_mtime} + new_item = {**item, "old_content": item.get("content", ""), "content": content, "error": False, "mtime": current_mtime} refreshed.append(new_item) changed.append(new_item) except Exception as e: @@ -486,6 +495,35 @@ def _build_file_context_text(file_items: list[dict]) -> str: return "\n\n---\n\n".join(parts) +_DIFF_LINE_THRESHOLD = 200 + +def _build_file_diff_text(changed_items: list[dict]) -> str: + """ + Build text for changed files. Small files (<= _DIFF_LINE_THRESHOLD lines) + get full content; large files get a unified diff against old_content. + """ + if not changed_items: + return "" + parts = [] + for item in changed_items: + path = item.get("path") or item.get("entry", "unknown") + content = item.get("content", "") + old_content = item.get("old_content", "") + new_lines = content.splitlines(keepends=True) + if len(new_lines) <= _DIFF_LINE_THRESHOLD or not old_content: + suffix = str(path).rsplit(".", 1)[-1] if "." in str(path) else "text" + parts.append(f"### `{path}` (full)\n\n```{suffix}\n{content}\n```") + else: + old_lines = old_content.splitlines(keepends=True) + diff = difflib.unified_diff(old_lines, new_lines, fromfile=str(path), tofile=str(path), lineterm="") + diff_text = "\n".join(diff) + if diff_text: + parts.append(f"### `{path}` (diff)\n\n```diff\n{diff_text}\n```") + else: + parts.append(f"### `{path}` (no changes detected)") + return "\n\n---\n\n".join(parts) + + # ------------------------------------------------------------------ content block serialisation def _content_block_to_dict(block) -> dict: @@ -530,22 +568,26 @@ def _get_gemini_history_list(chat): return chat.get_history() return [] -def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: list[dict] | None = None) -> str: +def _send_gemini(md_content: str, user_message: str, base_dir: str, + file_items: list[dict] | None = None, + discussion_history: str = "") -> str: global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at try: _ensure_gemini_client(); mcp_client.configure(file_items or [], [base_dir]) + # Only stable content (files + screenshots) goes in the cached system instruction. + # Discussion history is sent as conversation messages so the cache isn't invalidated every turn. sys_instr = f"{_get_combined_system_prompt()}\n\n\n{md_content}\n" tools_decl = [_gemini_tool_declaration()] # DYNAMIC CONTEXT: Check if files/context changed mid-session - current_md_hash = hash(md_content) + current_md_hash = hashlib.md5(md_content.encode()).hexdigest() old_history = None if _gemini_chat and _gemini_cache_md_hash != current_md_hash: old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else [] if _gemini_cache: try: _gemini_client.caches.delete(name=_gemini_cache.name) - except: pass + except Exception as e: _append_comms("OUT", "request", {"message": f"[CACHE DELETE WARN] {e}"}) _gemini_chat = None _gemini_cache = None _gemini_cache_created_at = None @@ -558,7 +600,7 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: if elapsed > _GEMINI_CACHE_TTL * 0.9: old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else [] try: _gemini_client.caches.delete(name=_gemini_cache.name) - except: pass + except Exception as e: _append_comms("OUT", "request", {"message": f"[CACHE DELETE WARN] {e}"}) _gemini_chat = None _gemini_cache = None _gemini_cache_created_at = None @@ -601,7 +643,13 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: _gemini_chat = _gemini_client.chats.create(**kwargs) _gemini_cache_md_hash = current_md_hash - + + # Inject discussion history as a user message on first chat creation + # (only when there's no old_history being restored, i.e., fresh session) + if discussion_history and not old_history: + _gemini_chat.send_message(f"[DISCUSSION HISTORY]\n\n{discussion_history}") + _append_comms("OUT", "request", {"message": f"[HISTORY INJECTED] {len(discussion_history)} chars"}) + _append_comms("OUT", "request", {"message": f"[ctx {len(md_content)} + msg {len(user_message)}]"}) payload, all_text = user_message, [] @@ -634,26 +682,19 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: if cached_tokens: usage["cache_read_input_tokens"] = cached_tokens - # Fetch cache stats in the background thread to avoid blocking GUI - cache_stats = None - try: - cache_stats = get_gemini_cache_stats() - except Exception: - pass - - events.emit("response_received", payload={"provider": "gemini", "model": _model, "usage": usage, "round": r_idx, "cache_stats": cache_stats}) + events.emit("response_received", payload={"provider": "gemini", "model": _model, "usage": usage, "round": r_idx}) reason = resp.candidates[0].finish_reason.name if resp.candidates and hasattr(resp.candidates[0], "finish_reason") else "STOP" _append_comms("IN", "response", {"round": r_idx, "stop_reason": reason, "text": txt, "tool_calls": [{"name": c.name, "args": dict(c.args)} for c in calls], "usage": usage}) - # Guard: if Gemini reports input tokens approaching the limit, drop oldest history pairs + # Guard: proactively trim history when input tokens exceed 40% of limit total_in = usage.get("input_tokens", 0) - if total_in > _GEMINI_MAX_INPUT_TOKENS and _gemini_chat and _get_gemini_history_list(_gemini_chat): + if total_in > _GEMINI_MAX_INPUT_TOKENS * 0.4 and _gemini_chat and _get_gemini_history_list(_gemini_chat): hist = _get_gemini_history_list(_gemini_chat) dropped = 0 # Drop oldest pairs (user+model) but keep at least the last 2 entries - while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.7: + while len(hist) > 4 and total_in > _GEMINI_MAX_INPUT_TOKENS * 0.3: # Drop in pairs (user + model) to maintain alternating roles required by Gemini saved = 0 for _ in range(2): @@ -689,11 +730,12 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items: if i == len(calls) - 1: if file_items: file_items, changed = _reread_file_items(file_items) - ctx = _build_file_context_text(changed) + ctx = _build_file_diff_text(changed) if ctx: out += f"\n\n[SYSTEM: FILES UPDATED]\n\n{ctx}" if r_idx == MAX_TOOL_ROUNDS: out += "\n\n[SYSTEM: MAX ROUNDS. PROVIDE FINAL ANSWER.]" + out = _truncate_tool_output(out) f_resps.append(types.Part.from_function_response(name=name, response={"output": out})) log.append({"tool_use_id": name, "content": out}) events.emit("tool_execution", payload={"status": "completed", "tool": name, "result": out, "round": r_idx}) @@ -955,7 +997,7 @@ def _repair_anthropic_history(history: list[dict]): }) -def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_items: list[dict] | None = None) -> str: +def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_items: list[dict] | None = None, discussion_history: str = "") -> str: try: _ensure_anthropic_client() mcp_client.configure(file_items or [], [base_dir]) @@ -969,7 +1011,11 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item context_blocks = _build_chunked_context_blocks(context_text) system_blocks = stable_blocks + context_blocks - user_content = [{"type": "text", "text": user_message}] + # Prepend discussion history to the first user message if this is a fresh session + if discussion_history and not _anthropic_history: + user_content = [{"type": "text", "text": f"[DISCUSSION HISTORY]\n\n{discussion_history}\n\n---\n\n{user_message}"}] + else: + user_content = [{"type": "text", "text": user_message}] # COMPRESS HISTORY: Truncate massive tool outputs from previous turns for msg in _anthropic_history: @@ -1089,7 +1135,7 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item tool_results.append({ "type": "tool_result", "tool_use_id": b_id, - "content": output, + "content": _truncate_tool_output(output), }) events.emit("tool_execution", payload={"status": "completed", "tool": b_name, "result": output, "round": round_idx}) elif b_name == TOOL_NAME: @@ -1108,14 +1154,14 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item tool_results.append({ "type": "tool_result", "tool_use_id": b_id, - "content": output, + "content": _truncate_tool_output(output), }) events.emit("tool_execution", payload={"status": "completed", "tool": b_name, "result": output, "round": round_idx}) # Refresh file context after tool calls — only inject CHANGED files if file_items: file_items, changed = _reread_file_items(file_items) - refreshed_ctx = _build_file_context_text(changed) + refreshed_ctx = _build_file_diff_text(changed) if refreshed_ctx: tool_results.append({ "type": "text", @@ -1160,20 +1206,24 @@ def send( user_message: str, base_dir: str = ".", file_items: list[dict] | None = None, + discussion_history: str = "", ) -> str: """ Send a message to the active provider. - md_content : aggregated markdown string from aggregate.run() - user_message: the user question / instruction - base_dir : project base directory (for PowerShell tool calls) - file_items : list of file dicts from aggregate.build_file_items() for - dynamic context refresh after tool calls + md_content : aggregated markdown string (for Gemini: stable content only, + for Anthropic: full content including history) + user_message : the user question / instruction + base_dir : project base directory (for PowerShell tool calls) + file_items : list of file dicts from aggregate.build_file_items() for + dynamic context refresh after tool calls + discussion_history : discussion history text (used by Gemini to inject as + conversation message instead of caching it) """ if _provider == "gemini": - return _send_gemini(md_content, user_message, base_dir, file_items) + return _send_gemini(md_content, user_message, base_dir, file_items, discussion_history) elif _provider == "anthropic": - return _send_anthropic(md_content, user_message, base_dir, file_items) + return _send_anthropic(md_content, user_message, base_dir, file_items, discussion_history) raise ValueError(f"unknown provider: {_provider}") def get_history_bleed_stats() -> dict: diff --git a/config.toml b/config.toml index 3114737..7578144 100644 --- a/config.toml +++ b/config.toml @@ -18,3 +18,17 @@ paths = [ "C:/projects/forth/bootslop/bootslop.toml", ] active = "manual_slop.toml" + +[gui.show_windows] +Projects = true +Files = true +Screenshots = true +"Discussion History" = true +Provider = true +Message = true +Response = true +"Tool Calls" = true +"Comms History" = true +"System Prompts" = true +Theme = true +Diagnostics = true diff --git a/gui_2.py b/gui_2.py index 845b75b..723b880 100644 --- a/gui_2.py +++ b/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() diff --git a/manual_slop.toml b/manual_slop.toml index 442219d..804e225 100644 --- a/manual_slop.toml +++ b/manual_slop.toml @@ -4,6 +4,7 @@ git_dir = "C:/projects/manual_slop" system_prompt = "Make sure to update MainContext.md every time.\nMake destructive modifications to the project, ITS OK, I HAVE GIT HISTORY TO MANAGE THE PROJECTS.\nAvoid reading manual_slop.toml its expensive as it has the history of multiple dicussions.\n" main_context = "C:/projects/manual_slop/MainContext.md" word_wrap = true +summary_only = false [output] namespace = "manual_slop" @@ -162,7 +163,7 @@ history = [ [discussion.discussions.docs] git_commit = "" -last_updated = "2026-02-22T22:55:04" +last_updated = "2026-02-23T19:57:04" history = [ "@2026-02-22T17:05:29\nUser:\nUpdate docs with latest state of the codebase files. ai_client.py and aggregate.py both recieved updates.", "@2026-02-22T17:06:43\nVendor API:\n[GEMINI QUOTA EXHAUSTED]\n\n429 RESOURCE_EXHAUSTED. {'error': {'code': 429, 'message': 'Resource has been exhausted (e.g. check quota).', 'status': 'RESOURCE_EXHAUSTED'}}", @@ -173,3 +174,12 @@ history = [ "@2026-02-22T17:23:07\nUser:\napologies you were trying to do a tool call and my panel glitched when I attempted to maximize the view, we problably need to also fix that (gui.py related).", "@2026-02-22T17:25:34\nAI:\nLet me read the current state of the relevant files to understand what happened and what needs to be updated.\n\nGood. Now I have the full picture. There are two tasks:\n\n1. **Update the docs** (`guide_architecture.md`, `guide_tools.md`, `docs/Readme.md`) to reflect the current state of `ai_client.py` (web tools, Gemini cache management, etc.)\n2. **Fix the GUI bug** — the `_show_text_viewer` callback crashes when called from a button inside the ConfirmDialog, because the `user_data` lambda captures the tag string but the `dpg.get_value()` call on that tag may fail or the wrong value gets passed.\n\nLet me look at the specific bug area in `gui.py` more carefully. The issue is in `ConfirmDialog.show()` — the `[+ Maximize]` button callback uses `dpg.get_value(u)` where `u` is the tag string `f\"{self._tag}_script\"`. But `_show_text_viewer` receives `u` as the tag, not the value. Let me check:\n\nIn `ConfirmDialog.show()`:\n```python\ndpg.add_button(\n label=\"[+ Maximize]\", \n user_data=f\"{self._tag}_script\",\n callback=lambda s, a, u: _show_text_viewer(\"Confirm Script\", dpg.get_value(u))\n)\n```\n\nThis looks correct actually — `u` is the tag, and `dpg.get_value(u)` reads from it. The issue is more likely that when the panel \"glitches\" during maximize, the `_show_text_viewer` function tries to configure `win_text_viewer` but the viewer window may not exist yet at that point, or the `text_viewer_content` tag doesn't exist. Let me look at `_show_text_viewer` — it checks `dpg.does_item_exist(\"win_text_viewer\")` but if the window doesn't exist it silently does nothing. That's actually the bug — when the text viewer window doesn't exist yet (e.g., first call before `_build_ui` completes, or if DPG hasn't rendered it yet), the function silently fails.\n\nBut more likely the crash is that `dpg.get_value(u)` is being called where `u` is `None` or the tag doesn't exist at the moment of click. The safest fix is to capture the value at button-creation time rather than reading it at click time for the ConfirmDialog case, and also add a guard in `_show_text_viewer` to create the window if it doesn't exist.\n\nActually, re-reading the user's report: \"my panel glitched when I attempted to maximize the view\" — this sounds like the Text Viewer window was opened but something went wrong with its content or the ConfirmDialog was dismissed/deleted before the viewer could read from it.\n\nThe real issue: when the user clicks `[+ Maximize]` in the ConfirmDialog, the lambda calls `dpg.get_value(f\"{self._tag}_script\")`. But if the dialog was already closed (or the item deleted), this will throw. The fix is to capture the value at callback time safely with a try/except, or better — pass the actual text content as `user_data` instead of the tag.\n\nLet me fix `ConfirmDialog.show()` to pass the script text directly as `user_data`:\n\nNow I have the full picture. I can see the bug clearly:\n\n**The bug in `ConfirmDialog.show()`**: The `[+ Maximize]` button's callback is `lambda s, a, u: _show_text_viewer(\"Confirm Script\", dpg.get_value(u))`. When the user clicks `[+ Maximize]`, `dpg.get_value(u)` reads the current value from the `input_text` widget tagged `f\"{self._tag}_script\"`. This works fine... **unless** the ConfirmDialog window was already deleted (approved/rejected) before the viewer was opened, or if the `win_text_viewer` doesn't exist yet at that point.\n\nBut the actual crash path is more subtle: the `[+ Maximize]` buttons in `win_script_output` use `callback=lambda s, a, u: _show_text_viewer(\"Last Script\", dpg.get_value(u))` where `u` is the string tag `\"last_script_text\"`. When word-wrap is ON, `last_script_text` is hidden (`show=False`) but still exists — so `dpg.get_value` works. But if the user somehow triggers this when the item doesn't exist, it crashes.\n\nThe real fix needed: The `[+ Maximize]` buttons in `win_script_output` should read from the actual stored value, not from `dpg.get_value(tag)` — because the wrap/no-wrap pair means the visible widget might be the wrap one, not the input_text. The `_tool_log` has the actual data. Also, the `ConfirmDialog` maximize button should capture the text value at call time, not via tag lookup.\n\nLet me fix both issues in `gui.py`:\n\n1. **`ConfirmDialog.show()`** — capture script text directly as `user_data` instead of reading via tag at click time.\n2. **`win_script_output` `[+ Maximize]` buttons** — read from the stored `_tool_log` last entry or use a safe fallback that checks both the input_text and wrap widget.\n\nI can see the first fix didn't apply — the file still has the old `ConfirmDialog` code. The `Replace()` failed silently because the here-string whitespace didn't match exactly. I'll use a targeted line-based approach instead.\n\nGood, I can see the exact text. The issue is the trailing space after `\"[+ Maximize]\", `. Let me do a precise replacement:\n\nGood, the ConfirmDialog fix is applied. Now I need to fix the second bug: the `[+ Maximize]` buttons in `win_script_output` use `dpg.get_value(u)` where `u` is the tag string. When word-wrap is ON, the `input_text` widget is hidden (`show=False`) — `dpg.get_value` on a hidden item still works in DPG, but the issue is that the wrap widget (`last_script_text_wrap`) is a `dpg.add_text` (not an `input_text`), so `dpg.get_value` on it returns an empty string. The fix is to read from `self._tool_log` directly via a stored reference, or better — store the last script/output as instance variables and pass them directly as `user_data`.\n\nThe cleanest fix: change the `[+ Maximize]` buttons in `win_script_output` to use a method-based callback that reads from `self._tool_log[-1]` at click time, rather than reading from a potentially-hidden widget.\n\nI'll add two helper methods to `App` and update the button callbacks:\n\nGood, I can see the current state. Now I need to fix the `win_script_output` `[+ Maximize]` buttons. The problem: when word-wrap is ON, `last_script_text` is hidden (`show=False`) but `dpg.get_value()` on a hidden `input_text` still works fine in DPG. However, the issue is that `dpg.get_value(\"last_script_output\")` returns `\"\"` when the widget is hidden in some DPG versions, OR the tag-based lookup `dpg.get_value(u)` is being called where `u` is the tag string — but the wrap variant is a `dpg.add_text` (not `input_text`), so if somehow the wrong widget is targeted, it returns empty.\n\nThe cleanest fix: update `_append_tool_log` to also store `self._last_script` and `self._last_output`, then change the `[+ Maximize]` buttons in `win_script_output` to use `self._last_script` / `self._last_output` via bound method callbacks instead of `dpg.get_value(tag)`.\n\nGood, the gui.py changes are in. Now I need to fix the remaining issue in `win_script_output` — the `[+ Maximize]` buttons still use `dpg.get_value(u)` where `u` is a tag string. When word-wrap is ON, `last_script_text` is hidden and `dpg.get_value` on a hidden `input_text` returns `\"\"` in DPG. The fix is to change those buttons to use `self._last_script` and `self._last_output` directly via bound method callbacks.\n\nGood, `gui.py` is correct. Now I need to update the docs and `MainContext.md`. Let me do the docs updates now, and the MainContext.md update after.\n\nThe `gui.py` changes are confirmed correct. Now let me update the three docs files and `MainContext.md` to reflect the current state of `ai_client.py` (web tools added, Gemini cache management, etc.) and the `gui.py` bug fix.", ] + +[agent.tools] +run_powershell = true +read_file = true +list_directory = true +search_files = true +get_file_summary = true +web_search = true +fetch_url = true diff --git a/manualslop_layout.ini b/manualslop_layout.ini index f9dc2b1..cc2bfc9 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -27,7 +27,7 @@ DockId=0x00000015,0 [Window][Discussion History] Pos=598,128 -Size=712,619 +Size=554,619 Collapsed=0 DockId=0x0000000E,0 @@ -39,7 +39,7 @@ DockId=0x0000000A,0 [Window][Message] Pos=598,749 -Size=712,451 +Size=554,451 Collapsed=0 DockId=0x0000000C,0 @@ -50,22 +50,22 @@ Collapsed=0 DockId=0x00000010,0 [Window][Tool Calls] -Pos=1312,733 -Size=368,144 +Pos=1154,733 +Size=526,144 Collapsed=0 DockId=0x00000008,0 [Window][Comms History] -Pos=1312,879 -Size=368,321 +Pos=1154,879 +Size=526,321 Collapsed=0 DockId=0x00000006,0 [Window][System Prompts] -Pos=1312,0 -Size=368,731 +Pos=1154,0 +Size=286,731 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000017,0 [Window][Theme] Pos=209,173 @@ -78,11 +78,17 @@ Pos=379,324 Size=900,700 Collapsed=0 +[Window][Diagnostics] +Pos=1442,0 +Size=238,731 +Collapsed=0 +DockId=0x00000018,0 + [Docking][Data] -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=138,161 Size=1680,1200 Split=X +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=346,232 Size=1680,1200 Split=X DockNode ID=0x00000011 Parent=0xAFC85805 SizeRef=207,1200 Selected=0x0469CA7A DockNode ID=0x00000012 Parent=0xAFC85805 SizeRef=1559,1200 Split=X - DockNode ID=0x00000003 Parent=0x00000012 SizeRef=1189,1200 Split=X + DockNode ID=0x00000003 Parent=0x00000012 SizeRef=943,1200 Split=X DockNode ID=0x00000001 Parent=0x00000003 SizeRef=387,1200 Split=Y Selected=0x8CA2375C DockNode ID=0x00000009 Parent=0x00000001 SizeRef=405,911 Split=Y Selected=0x8CA2375C DockNode ID=0x0000000F Parent=0x00000009 SizeRef=405,733 Split=Y Selected=0x8CA2375C @@ -92,14 +98,16 @@ DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=138,161 Size=1680,12 DockNode ID=0x00000014 Parent=0x0000000F SizeRef=405,337 Selected=0xDA22FEDA DockNode ID=0x00000010 Parent=0x00000009 SizeRef=405,176 Selected=0x0D5A5273 DockNode ID=0x0000000A Parent=0x00000001 SizeRef=405,287 Selected=0xA07B5F14 - DockNode ID=0x00000002 Parent=0x00000003 SizeRef=800,1200 Split=Y + DockNode ID=0x00000002 Parent=0x00000003 SizeRef=554,1200 Split=Y DockNode ID=0x0000000B Parent=0x00000002 SizeRef=1010,747 Split=Y DockNode ID=0x0000000D Parent=0x0000000B SizeRef=1010,126 CentralNode=1 DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1010,619 Selected=0x5D11106F DockNode ID=0x0000000C Parent=0x00000002 SizeRef=1010,451 Selected=0x66CFB56E - DockNode ID=0x00000004 Parent=0x00000012 SizeRef=368,1200 Split=Y Selected=0xDD6419BC + DockNode ID=0x00000004 Parent=0x00000012 SizeRef=526,1200 Split=Y Selected=0xDD6419BC DockNode ID=0x00000005 Parent=0x00000004 SizeRef=261,877 Split=Y Selected=0xDD6419BC - DockNode ID=0x00000007 Parent=0x00000005 SizeRef=261,731 Selected=0xDD6419BC + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=261,731 Split=X Selected=0xDD6419BC + DockNode ID=0x00000017 Parent=0x00000007 SizeRef=286,731 Selected=0xDD6419BC + DockNode ID=0x00000018 Parent=0x00000007 SizeRef=238,731 Selected=0xB4CBF21A DockNode ID=0x00000008 Parent=0x00000005 SizeRef=261,144 Selected=0x1D56B311 DockNode ID=0x00000006 Parent=0x00000004 SizeRef=261,321 Selected=0x8B4EBFA6