Merge branch 'origin/cache'
This commit is contained in:
15
aggregate.py
15
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)
|
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:
|
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 = []
|
parts = []
|
||||||
# STATIC PREFIX: Files and Screenshots must go first to maximize Cache Hits
|
# 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"
|
output_file = output_dir / f"{namespace}_{increment:03d}.md"
|
||||||
# Build file items once, then construct markdown from them (avoids double I/O)
|
# Build file items once, then construct markdown from them (avoids double I/O)
|
||||||
file_items = build_file_items(base_dir, files)
|
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,
|
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")
|
output_file.write_text(markdown, encoding="utf-8")
|
||||||
return markdown, output_file, file_items
|
return markdown, output_file, file_items
|
||||||
|
|
||||||
|
|||||||
100
ai_client.py
100
ai_client.py
@@ -15,6 +15,8 @@ import tomllib
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import difflib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import file_cache
|
import file_cache
|
||||||
import mcp_client
|
import mcp_client
|
||||||
@@ -435,6 +437,13 @@ def _run_script(script: str, base_dir: str) -> str:
|
|||||||
return output
|
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
|
# ------------------------------------------------------------------ dynamic file context refresh
|
||||||
|
|
||||||
def _reread_file_items(file_items: list[dict]) -> tuple[list[dict], list[dict]]:
|
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
|
refreshed.append(item) # unchanged — skip re-read
|
||||||
continue
|
continue
|
||||||
content = p.read_text(encoding="utf-8")
|
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)
|
refreshed.append(new_item)
|
||||||
changed.append(new_item)
|
changed.append(new_item)
|
||||||
except Exception as e:
|
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)
|
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
|
# ------------------------------------------------------------------ content block serialisation
|
||||||
|
|
||||||
def _content_block_to_dict(block) -> dict:
|
def _content_block_to_dict(block) -> dict:
|
||||||
@@ -530,22 +568,26 @@ def _get_gemini_history_list(chat):
|
|||||||
return chat.get_history()
|
return chat.get_history()
|
||||||
return []
|
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
|
global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_ensure_gemini_client(); mcp_client.configure(file_items or [], [base_dir])
|
_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<context>\n{md_content}\n</context>"
|
sys_instr = f"{_get_combined_system_prompt()}\n\n<context>\n{md_content}\n</context>"
|
||||||
tools_decl = [_gemini_tool_declaration()]
|
tools_decl = [_gemini_tool_declaration()]
|
||||||
|
|
||||||
# DYNAMIC CONTEXT: Check if files/context changed mid-session
|
# 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
|
old_history = None
|
||||||
if _gemini_chat and _gemini_cache_md_hash != current_md_hash:
|
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 []
|
old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else []
|
||||||
if _gemini_cache:
|
if _gemini_cache:
|
||||||
try: _gemini_client.caches.delete(name=_gemini_cache.name)
|
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_chat = None
|
||||||
_gemini_cache = None
|
_gemini_cache = None
|
||||||
_gemini_cache_created_at = 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:
|
if elapsed > _GEMINI_CACHE_TTL * 0.9:
|
||||||
old_history = list(_get_gemini_history_list(_gemini_chat)) if _get_gemini_history_list(_gemini_chat) else []
|
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)
|
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_chat = None
|
||||||
_gemini_cache = None
|
_gemini_cache = None
|
||||||
_gemini_cache_created_at = None
|
_gemini_cache_created_at = None
|
||||||
@@ -602,6 +644,12 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items:
|
|||||||
_gemini_chat = _gemini_client.chats.create(**kwargs)
|
_gemini_chat = _gemini_client.chats.create(**kwargs)
|
||||||
_gemini_cache_md_hash = current_md_hash
|
_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)}]"})
|
_append_comms("OUT", "request", {"message": f"[ctx {len(md_content)} + msg {len(user_message)}]"})
|
||||||
payload, all_text = 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:
|
if cached_tokens:
|
||||||
usage["cache_read_input_tokens"] = cached_tokens
|
usage["cache_read_input_tokens"] = cached_tokens
|
||||||
|
|
||||||
# Fetch cache stats in the background thread to avoid blocking GUI
|
events.emit("response_received", payload={"provider": "gemini", "model": _model, "usage": usage, "round": r_idx})
|
||||||
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})
|
|
||||||
|
|
||||||
reason = resp.candidates[0].finish_reason.name if resp.candidates and hasattr(resp.candidates[0], "finish_reason") else "STOP"
|
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})
|
_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)
|
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)
|
hist = _get_gemini_history_list(_gemini_chat)
|
||||||
dropped = 0
|
dropped = 0
|
||||||
# Drop oldest pairs (user+model) but keep at least the last 2 entries
|
# 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
|
# Drop in pairs (user + model) to maintain alternating roles required by Gemini
|
||||||
saved = 0
|
saved = 0
|
||||||
for _ in range(2):
|
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 i == len(calls) - 1:
|
||||||
if file_items:
|
if file_items:
|
||||||
file_items, changed = _reread_file_items(file_items)
|
file_items, changed = _reread_file_items(file_items)
|
||||||
ctx = _build_file_context_text(changed)
|
ctx = _build_file_diff_text(changed)
|
||||||
if ctx:
|
if ctx:
|
||||||
out += f"\n\n[SYSTEM: FILES UPDATED]\n\n{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.]"
|
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}))
|
f_resps.append(types.Part.from_function_response(name=name, response={"output": out}))
|
||||||
log.append({"tool_use_id": name, "content": out})
|
log.append({"tool_use_id": name, "content": out})
|
||||||
events.emit("tool_execution", payload={"status": "completed", "tool": name, "result": out, "round": r_idx})
|
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:
|
try:
|
||||||
_ensure_anthropic_client()
|
_ensure_anthropic_client()
|
||||||
mcp_client.configure(file_items or [], [base_dir])
|
mcp_client.configure(file_items or [], [base_dir])
|
||||||
@@ -969,6 +1011,10 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
|
|||||||
context_blocks = _build_chunked_context_blocks(context_text)
|
context_blocks = _build_chunked_context_blocks(context_text)
|
||||||
system_blocks = stable_blocks + context_blocks
|
system_blocks = stable_blocks + context_blocks
|
||||||
|
|
||||||
|
# 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}]
|
user_content = [{"type": "text", "text": user_message}]
|
||||||
|
|
||||||
# COMPRESS HISTORY: Truncate massive tool outputs from previous turns
|
# COMPRESS HISTORY: Truncate massive tool outputs from previous turns
|
||||||
@@ -1089,7 +1135,7 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
|
|||||||
tool_results.append({
|
tool_results.append({
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
"tool_use_id": b_id,
|
"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})
|
events.emit("tool_execution", payload={"status": "completed", "tool": b_name, "result": output, "round": round_idx})
|
||||||
elif b_name == TOOL_NAME:
|
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({
|
tool_results.append({
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
"tool_use_id": b_id,
|
"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})
|
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
|
# Refresh file context after tool calls — only inject CHANGED files
|
||||||
if file_items:
|
if file_items:
|
||||||
file_items, changed = _reread_file_items(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:
|
if refreshed_ctx:
|
||||||
tool_results.append({
|
tool_results.append({
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1160,20 +1206,24 @@ def send(
|
|||||||
user_message: str,
|
user_message: str,
|
||||||
base_dir: str = ".",
|
base_dir: str = ".",
|
||||||
file_items: list[dict] | None = None,
|
file_items: list[dict] | None = None,
|
||||||
|
discussion_history: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Send a message to the active provider.
|
Send a message to the active provider.
|
||||||
|
|
||||||
md_content : aggregated markdown string from aggregate.run()
|
md_content : aggregated markdown string (for Gemini: stable content only,
|
||||||
user_message: the user question / instruction
|
for Anthropic: full content including history)
|
||||||
|
user_message : the user question / instruction
|
||||||
base_dir : project base directory (for PowerShell tool calls)
|
base_dir : project base directory (for PowerShell tool calls)
|
||||||
file_items : list of file dicts from aggregate.build_file_items() for
|
file_items : list of file dicts from aggregate.build_file_items() for
|
||||||
dynamic context refresh after tool calls
|
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":
|
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":
|
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}")
|
raise ValueError(f"unknown provider: {_provider}")
|
||||||
|
|
||||||
def get_history_bleed_stats() -> dict:
|
def get_history_bleed_stats() -> dict:
|
||||||
|
|||||||
31
config.toml
31
config.toml
@@ -1,15 +1,16 @@
|
|||||||
|
# config.toml
|
||||||
[ai]
|
[ai]
|
||||||
provider = "gemini"
|
provider = "gemini"
|
||||||
model = "gemini-2.5-flash"
|
model = "gemini-1.5-pro-latest"
|
||||||
temperature = 0.6000000238418579
|
temperature = 0.0
|
||||||
max_tokens = 12000
|
max_tokens = 8192
|
||||||
history_trunc_limit = 8000
|
history_trunc_limit = 8000
|
||||||
system_prompt = "DO NOT EVER make a shell script unless told to. DO NOT EVER make a readme or a file describing your changes unless your are told to. If you have commands I should be entering into the command line or if you have something to explain to me, please just use code blocks or normal text output. DO NOT DO ANYTHING OTHER THAN WHAT YOU WERE TOLD TODO. DO NOT EVER, EVER DO ANYTHING OTHER THAN WHAT YOU WERE TOLD TO DO. IF YOU WANT TO DO OTHER THINGS, SIMPLY SUGGEST THEM, AND THEN I WILL REVIEW YOUR CHANGES, AND MAKE THE DECISION ON HOW TO PROCEED. WHEN WRITING SCRIPTS USE A 120-160 character limit per line. I don't want to see scrunched code.\n"
|
system_prompt = ""
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "10x Dark"
|
palette = "Gold"
|
||||||
font_path = "C:/Users/Ed/AppData/Local/uv/cache/archive-v0/WSthkYsQ82b_ywV6DkiaJ/pygame_gui/data/FiraCode-Regular.ttf"
|
font_path = ""
|
||||||
font_size = 18.0
|
font_size = 14.0
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
@@ -18,4 +19,18 @@ paths = [
|
|||||||
"C:/projects/forth/bootslop/bootslop.toml",
|
"C:/projects/forth/bootslop/bootslop.toml",
|
||||||
"C:\\projects\\manual_slop\\tests\\temp_project.toml",
|
"C:\\projects\\manual_slop\\tests\\temp_project.toml",
|
||||||
]
|
]
|
||||||
active = "C:\\projects\\manual_slop\\tests\\temp_project.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
|
||||||
|
|||||||
322
gui_2.py
322
gui_2.py
@@ -4,6 +4,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import filedialog, Tk
|
from tkinter import filedialog, Tk
|
||||||
import aggregate
|
import aggregate
|
||||||
@@ -15,6 +17,9 @@ import project_manager
|
|||||||
import theme_2 as theme
|
import theme_2 as theme
|
||||||
import tomllib
|
import tomllib
|
||||||
import events
|
import events
|
||||||
|
import numpy as np
|
||||||
|
import api_hooks
|
||||||
|
from performance_monitor import PerformanceMonitor
|
||||||
|
|
||||||
from imgui_bundle import imgui, hello_imgui, immapp
|
from imgui_bundle import imgui, hello_imgui, immapp
|
||||||
|
|
||||||
@@ -57,6 +62,15 @@ KIND_COLORS = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_res
|
|||||||
HEAVY_KEYS = {"message", "text", "script", "output", "content"}
|
HEAVY_KEYS = {"message", "text", "script", "output", "content"}
|
||||||
|
|
||||||
DISC_ROLES = ["User", "AI", "Vendor API", "System"]
|
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]:
|
def _parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict]:
|
||||||
known = roles if roles is not None else DISC_ROLES
|
known = roles if roles is not None else DISC_ROLES
|
||||||
@@ -120,6 +134,7 @@ class App:
|
|||||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||||
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
||||||
self.ui_word_wrap = proj_meta.get("word_wrap", True)
|
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_auto_add_history = disc_sec.get("auto_add", False)
|
||||||
|
|
||||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||||
@@ -140,7 +155,7 @@ class App:
|
|||||||
self.send_thread: threading.Thread | None = None
|
self.send_thread: threading.Thread | None = None
|
||||||
self.models_thread: threading.Thread | None = None
|
self.models_thread: threading.Thread | None = None
|
||||||
|
|
||||||
self.show_windows = {
|
_default_windows = {
|
||||||
"Projects": True,
|
"Projects": True,
|
||||||
"Files": True,
|
"Files": True,
|
||||||
"Screenshots": True,
|
"Screenshots": True,
|
||||||
@@ -152,7 +167,10 @@ class App:
|
|||||||
"Comms History": True,
|
"Comms History": True,
|
||||||
"System Prompts": True,
|
"System Prompts": True,
|
||||||
"Theme": 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_script_output = False
|
||||||
self.show_text_viewer = False
|
self.show_text_viewer = False
|
||||||
self.text_viewer_title = ""
|
self.text_viewer_title = ""
|
||||||
@@ -182,21 +200,49 @@ class App:
|
|||||||
|
|
||||||
self._scroll_disc_to_bottom = False
|
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()
|
session_logger.open_session()
|
||||||
ai_client.set_provider(self.current_provider, self.current_model)
|
ai_client.set_provider(self.current_provider, self.current_model)
|
||||||
ai_client.confirm_and_run_callback = self._confirm_and_run
|
ai_client.confirm_and_run_callback = self._confirm_and_run
|
||||||
ai_client.comms_log_callback = self._on_comms_entry
|
ai_client.comms_log_callback = self._on_comms_entry
|
||||||
ai_client.tool_log_callback = self._on_tool_log
|
ai_client.tool_log_callback = self._on_tool_log
|
||||||
|
|
||||||
# Subscribe to API lifecycle events
|
# AI client event subscriptions
|
||||||
ai_client.events.on("request_start", self._on_api_event)
|
ai_client.events.on("request_start", self._on_api_event)
|
||||||
ai_client.events.on("response_received", self._on_api_event)
|
ai_client.events.on("response_received", self._on_api_event)
|
||||||
ai_client.events.on("tool_execution", self._on_api_event)
|
ai_client.events.on("tool_execution", self._on_api_event)
|
||||||
|
|
||||||
def _on_api_event(self, *args, **kwargs):
|
|
||||||
"""Callback for ai_client events. For now, a placeholder."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- project loading
|
# ---------------------------------------------------------------- project loading
|
||||||
|
|
||||||
def _load_active_project(self):
|
def _load_active_project(self):
|
||||||
@@ -263,6 +309,10 @@ class App:
|
|||||||
self.ui_project_main_context = proj.get("project", {}).get("main_context", "")
|
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_auto_add_history = proj.get("discussion", {}).get("auto_add", False)
|
||||||
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
|
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):
|
def _save_active_project(self):
|
||||||
if self.active_project_path:
|
if self.active_project_path:
|
||||||
@@ -347,6 +397,76 @@ class App:
|
|||||||
def _on_tool_log(self, script: str, result: str):
|
def _on_tool_log(self, script: str, result: str):
|
||||||
session_logger.log_tool_call(script, result, None)
|
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:
|
def _confirm_and_run(self, script: str, base_dir: str) -> str | None:
|
||||||
dialog = ConfirmDialog(script, base_dir)
|
dialog = ConfirmDialog(script, base_dir)
|
||||||
with self._pending_dialog_lock:
|
with self._pending_dialog_lock:
|
||||||
@@ -383,6 +503,11 @@ class App:
|
|||||||
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
proj["project"]["system_prompt"] = self.ui_project_system_prompt
|
||||||
proj["project"]["main_context"] = self.ui_project_main_context
|
proj["project"]["main_context"] = self.ui_project_main_context
|
||||||
proj["project"]["word_wrap"] = self.ui_word_wrap
|
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()
|
self._flush_disc_entries_to_project()
|
||||||
disc_sec = proj.setdefault("discussion", {})
|
disc_sec = proj.setdefault("discussion", {})
|
||||||
@@ -400,15 +525,26 @@ class App:
|
|||||||
}
|
}
|
||||||
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
||||||
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
|
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)
|
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._flush_to_project()
|
||||||
self._save_active_project()
|
self._save_active_project()
|
||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
flat = project_manager.flat_config(self.project, self.active_discussion)
|
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):
|
def _fetch_models(self, provider: str):
|
||||||
self.ai_status = "fetching models..."
|
self.ai_status = "fetching models..."
|
||||||
@@ -455,6 +591,11 @@ class App:
|
|||||||
# ---------------------------------------------------------------- gui
|
# ---------------------------------------------------------------- gui
|
||||||
|
|
||||||
def _gui_func(self):
|
def _gui_func(self):
|
||||||
|
self.perf_monitor.start_frame()
|
||||||
|
|
||||||
|
# Process GUI task queue
|
||||||
|
self._process_pending_gui_tasks()
|
||||||
|
|
||||||
# Sync pending comms
|
# Sync pending comms
|
||||||
with self._pending_comms_lock:
|
with self._pending_comms_lock:
|
||||||
for c in self._pending_comms:
|
for c in self._pending_comms:
|
||||||
@@ -586,6 +727,14 @@ class App:
|
|||||||
self.ai_status = "config saved"
|
self.ai_status = "config saved"
|
||||||
|
|
||||||
ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap)
|
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()
|
imgui.end()
|
||||||
|
|
||||||
# ---- Files
|
# ---- Files
|
||||||
@@ -665,7 +814,50 @@ class App:
|
|||||||
if self.show_windows["Discussion History"]:
|
if self.show_windows["Discussion History"]:
|
||||||
exp, self.show_windows["Discussion History"] = imgui.begin("Discussion History", self.show_windows["Discussion History"])
|
exp, self.show_windows["Discussion History"] = imgui.begin("Discussion History", self.show_windows["Discussion History"])
|
||||||
if exp:
|
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()
|
names = self._get_discussion_names()
|
||||||
|
|
||||||
if imgui.begin_combo("##disc_sel", self.active_discussion):
|
if imgui.begin_combo("##disc_sel", self.active_discussion):
|
||||||
@@ -712,6 +904,7 @@ class App:
|
|||||||
if imgui.button("Delete"):
|
if imgui.button("Delete"):
|
||||||
self._delete_discussion(self.active_discussion)
|
self._delete_discussion(self.active_discussion)
|
||||||
|
|
||||||
|
if not self.is_viewing_prior_session:
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.button("+ Entry"):
|
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()})
|
self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()})
|
||||||
@@ -731,8 +924,22 @@ class App:
|
|||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
self.ai_status = "discussion saved"
|
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)
|
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()
|
imgui.separator()
|
||||||
|
|
||||||
if imgui.collapsing_header("Roles"):
|
if imgui.collapsing_header("Roles"):
|
||||||
@@ -846,12 +1053,32 @@ class App:
|
|||||||
ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f")
|
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.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)
|
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()
|
imgui.end()
|
||||||
|
|
||||||
# ---- Message
|
# ---- Message
|
||||||
if self.show_windows["Message"]:
|
if self.show_windows["Message"]:
|
||||||
exp, self.show_windows["Message"] = imgui.begin("Message", self.show_windows["Message"])
|
exp, self.show_windows["Message"] = imgui.begin("Message", self.show_windows["Message"])
|
||||||
if exp:
|
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))
|
ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40))
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.button("Gen + Send"):
|
if imgui.button("Gen + Send"):
|
||||||
@@ -869,14 +1096,20 @@ class App:
|
|||||||
base_dir = self.ui_files_base_dir
|
base_dir = self.ui_files_base_dir
|
||||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
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_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():
|
def do_send():
|
||||||
if self.ui_auto_add_history:
|
if self.ui_auto_add_history:
|
||||||
with self._pending_history_adds_lock:
|
with self._pending_history_adds_lock:
|
||||||
self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()})
|
self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()})
|
||||||
try:
|
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_response = resp
|
||||||
self.ai_status = "done"
|
self.ai_status = "done"
|
||||||
self._trigger_blink = True
|
self._trigger_blink = True
|
||||||
@@ -1178,6 +1411,67 @@ class App:
|
|||||||
if ch: theme.set_scale(scale)
|
if ch: theme.set_scale(scale)
|
||||||
imgui.end()
|
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
|
# ---- Modals / Popups
|
||||||
with self._pending_dialog_lock:
|
with self._pending_dialog_lock:
|
||||||
dlg = self._pending_dialog
|
dlg = self._pending_dialog
|
||||||
@@ -1304,9 +1598,15 @@ class App:
|
|||||||
|
|
||||||
self._fetch_models(self.current_provider)
|
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)
|
immapp.run(self.runner_params)
|
||||||
|
|
||||||
# On exit
|
# On exit
|
||||||
|
self.hook_server.stop()
|
||||||
|
self.perf_monitor.stop()
|
||||||
ai_client.cleanup() # Destroy active API caches to stop billing
|
ai_client.cleanup() # Destroy active API caches to stop billing
|
||||||
self._flush_to_project()
|
self._flush_to_project()
|
||||||
self._save_active_project()
|
self._save_active_project()
|
||||||
|
|||||||
156
manual_slop.toml
156
manual_slop.toml
File diff suppressed because one or more lines are too long
@@ -27,7 +27,7 @@ DockId=0x00000015,0
|
|||||||
|
|
||||||
[Window][Discussion History]
|
[Window][Discussion History]
|
||||||
Pos=598,128
|
Pos=598,128
|
||||||
Size=712,619
|
Size=554,619
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000E,0
|
DockId=0x0000000E,0
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ DockId=0x0000000A,0
|
|||||||
|
|
||||||
[Window][Message]
|
[Window][Message]
|
||||||
Pos=598,749
|
Pos=598,749
|
||||||
Size=712,451
|
Size=554,451
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000C,0
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
@@ -50,22 +50,22 @@ Collapsed=0
|
|||||||
DockId=0x00000010,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
Pos=1312,733
|
Pos=1154,733
|
||||||
Size=368,144
|
Size=526,144
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000008,0
|
DockId=0x00000008,0
|
||||||
|
|
||||||
[Window][Comms History]
|
[Window][Comms History]
|
||||||
Pos=1312,879
|
Pos=1154,879
|
||||||
Size=368,321
|
Size=526,321
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
[Window][System Prompts]
|
[Window][System Prompts]
|
||||||
Pos=1312,0
|
Pos=1154,0
|
||||||
Size=368,731
|
Size=286,731
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000007,0
|
DockId=0x00000017,0
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=209,173
|
Pos=209,173
|
||||||
@@ -78,11 +78,17 @@ Pos=379,324
|
|||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Diagnostics]
|
||||||
|
Pos=1442,0
|
||||||
|
Size=238,731
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000018,0
|
||||||
|
|
||||||
[Docking][Data]
|
[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=0x00000011 Parent=0xAFC85805 SizeRef=207,1200 Selected=0x0469CA7A
|
||||||
DockNode ID=0x00000012 Parent=0xAFC85805 SizeRef=1559,1200 Split=X
|
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=0x00000001 Parent=0x00000003 SizeRef=387,1200 Split=Y Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=405,911 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
|
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=0x00000014 Parent=0x0000000F SizeRef=405,337 Selected=0xDA22FEDA
|
||||||
DockNode ID=0x00000010 Parent=0x00000009 SizeRef=405,176 Selected=0x0D5A5273
|
DockNode ID=0x00000010 Parent=0x00000009 SizeRef=405,176 Selected=0x0D5A5273
|
||||||
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=405,287 Selected=0xA07B5F14
|
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=0x0000000B Parent=0x00000002 SizeRef=1010,747 Split=Y
|
||||||
DockNode ID=0x0000000D Parent=0x0000000B SizeRef=1010,126 CentralNode=1
|
DockNode ID=0x0000000D Parent=0x0000000B SizeRef=1010,126 CentralNode=1
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1010,619 Selected=0x5D11106F
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1010,619 Selected=0x5D11106F
|
||||||
DockNode ID=0x0000000C Parent=0x00000002 SizeRef=1010,451 Selected=0x66CFB56E
|
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=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=0x00000008 Parent=0x00000005 SizeRef=261,144 Selected=0x1D56B311
|
||||||
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=261,321 Selected=0x8B4EBFA6
|
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=261,321 Selected=0x8B4EBFA6
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user