Compare commits
55 Commits
a359f19cdc
...
sim
| Author | SHA1 | Date | |
|---|---|---|---|
| fb1117becc | |||
| df90bad4a1 | |||
| 9f2ed38845 | |||
| 59f4df4475 | |||
| c4da60d1c5 | |||
| 47c4117763 | |||
| 8e63b31508 | |||
| 8bd280efc1 | |||
| 75e1cf84fe | |||
| ba97ccda3c | |||
| 0f04e066ef | |||
| 5e1b965311 | |||
| fdb9b59d36 | |||
| 9c4a72c734 | |||
| 6d16438477 | |||
| bd5dc16715 | |||
| 895004ddc5 | |||
| 76265319a7 | |||
| bfe9ef014d | |||
| d326242667 | |||
| f36d539c36 | |||
| 1d674c3a1e | |||
| 1db5ac57ec | |||
| d8e42a697b | |||
| 050d995660 | |||
| 0c5ac55053 | |||
| 450c17b96e | |||
| 36ab691fbf | |||
| 8cca046d96 | |||
| 22f8943619 | |||
| 5257db5aca | |||
| ebd81586bb | |||
| ae5dd328e1 | |||
| b3cf58adb4 | |||
| 4a4cf8c14b | |||
| e3767d2994 | |||
| c5d54cfae2 | |||
| 975fcde9bd | |||
| 97367fe537 | |||
| 72c898e8c2 | |||
| f8fb58db1f | |||
| c341de5515 | |||
| b1687f4a6b | |||
| 6a35da1eb2 | |||
| 0e06956d63 | |||
| 8448c71287 | |||
| d177c0bf3c | |||
| 040fec3613 | |||
| e757922c72 | |||
| 05cd1b6596 | |||
| e9126b47db | |||
| 0f9f235438 | |||
| f0eb5382fe | |||
| 842bfc407c | |||
| 5ec4283f41 |
+53
-30
@@ -18,7 +18,8 @@ import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import file_cache
|
import file_cache
|
||||||
import mcp_client
|
import mcp_client
|
||||||
import google.genai
|
import anthropic
|
||||||
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
from events import EventEmitter
|
from events import EventEmitter
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ class ProviderError(Exception):
|
|||||||
|
|
||||||
def _classify_anthropic_error(exc: Exception) -> ProviderError:
|
def _classify_anthropic_error(exc: Exception) -> ProviderError:
|
||||||
try:
|
try:
|
||||||
import anthropic
|
|
||||||
if isinstance(exc, anthropic.RateLimitError):
|
if isinstance(exc, anthropic.RateLimitError):
|
||||||
return ProviderError("rate_limit", "anthropic", exc)
|
return ProviderError("rate_limit", "anthropic", exc)
|
||||||
if isinstance(exc, anthropic.AuthenticationError):
|
if isinstance(exc, anthropic.AuthenticationError):
|
||||||
@@ -276,9 +277,9 @@ def list_models(provider: str) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _list_gemini_models(api_key: str) -> list[str]:
|
def _list_gemini_models(api_key: str) -> list[str]:
|
||||||
# from google import genai # Removed
|
|
||||||
try:
|
try:
|
||||||
client = google.genai.Client(api_key=api_key)
|
client = genai.Client(api_key=api_key)
|
||||||
models = []
|
models = []
|
||||||
for m in client.models.list():
|
for m in client.models.list():
|
||||||
name = m.name
|
name = m.name
|
||||||
@@ -292,7 +293,7 @@ def _list_gemini_models(api_key: str) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _list_anthropic_models() -> list[str]:
|
def _list_anthropic_models() -> list[str]:
|
||||||
import anthropic
|
|
||||||
try:
|
try:
|
||||||
creds = _load_credentials()
|
creds = _load_credentials()
|
||||||
client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
|
client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
|
||||||
@@ -370,7 +371,7 @@ def _get_anthropic_tools() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def _gemini_tool_declaration():
|
def _gemini_tool_declaration():
|
||||||
# from google.genai import types # Removed
|
|
||||||
|
|
||||||
declarations = []
|
declarations = []
|
||||||
|
|
||||||
@@ -380,15 +381,17 @@ def _gemini_tool_declaration():
|
|||||||
continue
|
continue
|
||||||
props = {}
|
props = {}
|
||||||
for pname, pdef in spec["parameters"].get("properties", {}).items():
|
for pname, pdef in spec["parameters"].get("properties", {}).items():
|
||||||
props[pname] = google.genai.types.Schema(
|
ptype_str = pdef.get("type", "string").upper()
|
||||||
type=google.genai.types.Type.STRING,
|
ptype = getattr(types.Type, ptype_str, types.Type.STRING)
|
||||||
|
props[pname] = types.Schema(
|
||||||
|
type=ptype,
|
||||||
description=pdef.get("description", ""),
|
description=pdef.get("description", ""),
|
||||||
)
|
)
|
||||||
declarations.append(google.genai.types.FunctionDeclaration(
|
declarations.append(types.FunctionDeclaration(
|
||||||
name=spec["name"],
|
name=spec["name"],
|
||||||
description=spec["description"],
|
description=spec["description"],
|
||||||
parameters=google.genai.types.Schema(
|
parameters=types.Schema(
|
||||||
type=google.genai.types.Type.OBJECT,
|
type=types.Type.OBJECT,
|
||||||
properties=props,
|
properties=props,
|
||||||
required=spec["parameters"].get("required", []),
|
required=spec["parameters"].get("required", []),
|
||||||
),
|
),
|
||||||
@@ -396,7 +399,7 @@ def _gemini_tool_declaration():
|
|||||||
|
|
||||||
# PowerShell tool
|
# PowerShell tool
|
||||||
if _agent_tools.get(TOOL_NAME, True):
|
if _agent_tools.get(TOOL_NAME, True):
|
||||||
declarations.append(google.genai.types.FunctionDeclaration(
|
declarations.append(types.FunctionDeclaration(
|
||||||
name=TOOL_NAME,
|
name=TOOL_NAME,
|
||||||
description=(
|
description=(
|
||||||
"Run a PowerShell script within the project base_dir. "
|
"Run a PowerShell script within the project base_dir. "
|
||||||
@@ -404,11 +407,11 @@ def _gemini_tool_declaration():
|
|||||||
"The working directory is set to base_dir automatically. "
|
"The working directory is set to base_dir automatically. "
|
||||||
"stdout and stderr are returned to you as the result."
|
"stdout and stderr are returned to you as the result."
|
||||||
),
|
),
|
||||||
parameters=google.genai.types.Schema(
|
parameters=types.Schema(
|
||||||
type=google.genai.types.Type.OBJECT,
|
type=types.Type.OBJECT,
|
||||||
properties={
|
properties={
|
||||||
"script": google.genai.types.Schema(
|
"script": types.Schema(
|
||||||
type=google.genai.types.Type.STRING,
|
type=types.Type.STRING,
|
||||||
description="The PowerShell script to execute."
|
description="The PowerShell script to execute."
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -416,7 +419,7 @@ def _gemini_tool_declaration():
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
||||||
return google.genai.types.Tool(function_declarations=declarations) if declarations else None
|
return types.Tool(function_declarations=declarations) if declarations else None
|
||||||
|
|
||||||
|
|
||||||
def _run_script(script: str, base_dir: str) -> str:
|
def _run_script(script: str, base_dir: str) -> str:
|
||||||
@@ -511,9 +514,8 @@ def _content_block_to_dict(block) -> dict:
|
|||||||
def _ensure_gemini_client():
|
def _ensure_gemini_client():
|
||||||
global _gemini_client
|
global _gemini_client
|
||||||
if _gemini_client is None:
|
if _gemini_client is None:
|
||||||
# from google import genai # Removed
|
|
||||||
creds = _load_credentials()
|
creds = _load_credentials()
|
||||||
_gemini_client = google.genai.Client(api_key=creds["gemini"]["api_key"])
|
_gemini_client = genai.Client(api_key=creds["gemini"]["api_key"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -530,7 +532,7 @@ def _get_gemini_history_list(chat):
|
|||||||
|
|
||||||
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) -> 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
|
||||||
# from google.genai import types # Removed
|
|
||||||
try:
|
try:
|
||||||
_ensure_gemini_client(); mcp_client.configure(file_items or [], [base_dir])
|
_ensure_gemini_client(); mcp_client.configure(file_items or [], [base_dir])
|
||||||
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>"
|
||||||
@@ -563,29 +565,29 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items:
|
|||||||
_append_comms("OUT", "request", {"message": f"[CACHE TTL] Rebuilding cache (expired after {int(elapsed)}s)..."})
|
_append_comms("OUT", "request", {"message": f"[CACHE TTL] Rebuilding cache (expired after {int(elapsed)}s)..."})
|
||||||
|
|
||||||
if not _gemini_chat:
|
if not _gemini_chat:
|
||||||
chat_config = google.genai.types.GenerateContentConfig(
|
chat_config = types.GenerateContentConfig(
|
||||||
system_instruction=sys_instr,
|
system_instruction=sys_instr,
|
||||||
tools=tools_decl,
|
tools=tools_decl,
|
||||||
temperature=_temperature,
|
temperature=_temperature,
|
||||||
max_output_tokens=_max_tokens,
|
max_output_tokens=_max_tokens,
|
||||||
safety_settings=[google.genai.types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_ONLY_HIGH")]
|
safety_settings=[types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_ONLY_HIGH")]
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
# Gemini requires 1024 (Flash) or 4096 (Pro) tokens to cache.
|
# Gemini requires 1024 (Flash) or 4096 (Pro) tokens to cache.
|
||||||
_gemini_cache = _gemini_client.caches.create(
|
_gemini_cache = _gemini_client.caches.create(
|
||||||
model=_model,
|
model=_model,
|
||||||
config=google.genai.types.CreateCachedContentConfig(
|
config=types.CreateCachedContentConfig(
|
||||||
system_instruction=sys_instr,
|
system_instruction=sys_instr,
|
||||||
tools=tools_decl,
|
tools=tools_decl,
|
||||||
ttl=f"{_GEMINI_CACHE_TTL}s",
|
ttl=f"{_GEMINI_CACHE_TTL}s",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_gemini_cache_created_at = time.time()
|
_gemini_cache_created_at = time.time()
|
||||||
chat_config = google.genai.types.GenerateContentConfig(
|
chat_config = types.GenerateContentConfig(
|
||||||
cached_content=_gemini_cache.name,
|
cached_content=_gemini_cache.name,
|
||||||
temperature=_temperature,
|
temperature=_temperature,
|
||||||
max_output_tokens=_max_tokens,
|
max_output_tokens=_max_tokens,
|
||||||
safety_settings=[google.genai.types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_ONLY_HIGH")]
|
safety_settings=[types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_ONLY_HIGH")]
|
||||||
)
|
)
|
||||||
_append_comms("OUT", "request", {"message": f"[CACHE CREATED] {_gemini_cache.name}"})
|
_append_comms("OUT", "request", {"message": f"[CACHE CREATED] {_gemini_cache.name}"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -857,9 +859,12 @@ def _trim_anthropic_history(system_blocks: list[dict], history: list[dict]):
|
|||||||
def _ensure_anthropic_client():
|
def _ensure_anthropic_client():
|
||||||
global _anthropic_client
|
global _anthropic_client
|
||||||
if _anthropic_client is None:
|
if _anthropic_client is None:
|
||||||
import anthropic
|
|
||||||
creds = _load_credentials()
|
creds = _load_credentials()
|
||||||
_anthropic_client = anthropic.Anthropic(api_key=creds["anthropic"]["api_key"])
|
# Enable prompt caching beta
|
||||||
|
_anthropic_client = anthropic.Anthropic(
|
||||||
|
api_key=creds["anthropic"]["api_key"],
|
||||||
|
default_headers={"anthropic-beta": "prompt-caching-2024-07-31"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _chunk_text(text: str, chunk_size: int) -> list[str]:
|
def _chunk_text(text: str, chunk_size: int) -> list[str]:
|
||||||
@@ -1187,9 +1192,27 @@ def get_history_bleed_stats() -> dict:
|
|||||||
"percentage": percentage,
|
"percentage": percentage,
|
||||||
}
|
}
|
||||||
elif _provider == "gemini":
|
elif _provider == "gemini":
|
||||||
# For Gemini, token estimation is complex and handled by the server.
|
if _gemini_chat:
|
||||||
# We don't have a reliable client-side estimate, so we return a
|
try:
|
||||||
# "not implemented" state for now.
|
_ensure_gemini_client()
|
||||||
|
history = _get_gemini_history_list(_gemini_chat)
|
||||||
|
if history:
|
||||||
|
resp = _gemini_client.models.count_tokens(
|
||||||
|
model=_model,
|
||||||
|
contents=history
|
||||||
|
)
|
||||||
|
current_tokens = resp.total_tokens
|
||||||
|
limit_tokens = _GEMINI_MAX_INPUT_TOKENS
|
||||||
|
percentage = (current_tokens / limit_tokens) * 100 if limit_tokens > 0 else 0
|
||||||
|
return {
|
||||||
|
"provider": "gemini",
|
||||||
|
"limit": limit_tokens,
|
||||||
|
"current": current_tokens,
|
||||||
|
"percentage": percentage,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"limit": _GEMINI_MAX_INPUT_TOKENS,
|
"limit": _GEMINI_MAX_INPUT_TOKENS,
|
||||||
|
|||||||
+53
-3
@@ -3,7 +3,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
class ApiHookClient:
|
class ApiHookClient:
|
||||||
def __init__(self, base_url="http://127.0.0.1:8999", max_retries=3, retry_delay=1):
|
def __init__(self, base_url="http://127.0.0.1:8999", max_retries=5, retry_delay=2):
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.max_retries = max_retries
|
self.max_retries = max_retries
|
||||||
self.retry_delay = retry_delay
|
self.retry_delay = retry_delay
|
||||||
@@ -29,9 +29,9 @@ class ApiHookClient:
|
|||||||
for attempt in range(self.max_retries + 1):
|
for attempt in range(self.max_retries + 1):
|
||||||
try:
|
try:
|
||||||
if method == 'GET':
|
if method == 'GET':
|
||||||
response = requests.get(url, timeout=2)
|
response = requests.get(url, timeout=5)
|
||||||
elif method == 'POST':
|
elif method == 'POST':
|
||||||
response = requests.post(url, json=data, headers=headers, timeout=2)
|
response = requests.post(url, json=data, headers=headers, timeout=5)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||||
|
|
||||||
@@ -83,3 +83,53 @@ class ApiHookClient:
|
|||||||
|
|
||||||
def post_gui(self, gui_data):
|
def post_gui(self, gui_data):
|
||||||
return self._make_request('POST', '/api/gui', data=gui_data)
|
return self._make_request('POST', '/api/gui', data=gui_data)
|
||||||
|
|
||||||
|
def select_tab(self, tab_bar, tab):
|
||||||
|
"""Tells the GUI to switch to a specific tab in a tab bar."""
|
||||||
|
return self.post_gui({
|
||||||
|
"action": "select_tab",
|
||||||
|
"tab_bar": tab_bar,
|
||||||
|
"tab": tab
|
||||||
|
})
|
||||||
|
|
||||||
|
def select_list_item(self, listbox, item_value):
|
||||||
|
"""Tells the GUI to select an item in a listbox by its value."""
|
||||||
|
return self.post_gui({
|
||||||
|
"action": "select_list_item",
|
||||||
|
"listbox": listbox,
|
||||||
|
"item_value": item_value
|
||||||
|
})
|
||||||
|
|
||||||
|
def set_value(self, item, value):
|
||||||
|
"""Sets the value of a GUI item."""
|
||||||
|
return self.post_gui({
|
||||||
|
"action": "set_value",
|
||||||
|
"item": item,
|
||||||
|
"value": value
|
||||||
|
})
|
||||||
|
|
||||||
|
def click(self, item, *args, **kwargs):
|
||||||
|
"""Simulates a click on a GUI button or item."""
|
||||||
|
user_data = kwargs.pop('user_data', None)
|
||||||
|
return self.post_gui({
|
||||||
|
"action": "click",
|
||||||
|
"item": item,
|
||||||
|
"args": args,
|
||||||
|
"kwargs": kwargs,
|
||||||
|
"user_data": user_data
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_indicator_state(self, tag):
|
||||||
|
"""Checks if an indicator is shown using the diagnostics endpoint."""
|
||||||
|
# Mapping tag to the keys used in diagnostics endpoint
|
||||||
|
mapping = {
|
||||||
|
"thinking_indicator": "thinking",
|
||||||
|
"operations_live_indicator": "live",
|
||||||
|
"prior_session_indicator": "prior"
|
||||||
|
}
|
||||||
|
key = mapping.get(tag, tag)
|
||||||
|
try:
|
||||||
|
diag = self._make_request('GET', '/api/gui/diagnostics')
|
||||||
|
return {"tag": tag, "shown": diag.get(key, False)}
|
||||||
|
except Exception as e:
|
||||||
|
return {"tag": tag, "shown": False, "error": str(e)}
|
||||||
|
|||||||
+39
-7
@@ -21,11 +21,12 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
|
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
|
||||||
elif self.path == '/api/project':
|
elif self.path == '/api/project':
|
||||||
|
import project_manager
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(
|
flat = project_manager.flat_config(app.project)
|
||||||
json.dumps({'project': app.project}).encode('utf-8'))
|
self.wfile.write(json.dumps({'project': flat}).encode('utf-8'))
|
||||||
elif self.path == '/api/session':
|
elif self.path == '/api/session':
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
@@ -41,6 +42,35 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
if hasattr(app, 'perf_monitor'):
|
if hasattr(app, 'perf_monitor'):
|
||||||
metrics = app.perf_monitor.get_metrics()
|
metrics = app.perf_monitor.get_metrics()
|
||||||
self.wfile.write(json.dumps({'performance': metrics}).encode('utf-8'))
|
self.wfile.write(json.dumps({'performance': metrics}).encode('utf-8'))
|
||||||
|
elif self.path == '/api/gui/diagnostics':
|
||||||
|
# Safe way to query multiple states at once via the main thread queue
|
||||||
|
event = threading.Event()
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
def check_all():
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
try:
|
||||||
|
result["thinking"] = dpg.is_item_shown("thinking_indicator") if dpg.does_item_exist("thinking_indicator") else False
|
||||||
|
result["live"] = dpg.is_item_shown("operations_live_indicator") if dpg.does_item_exist("operations_live_indicator") else False
|
||||||
|
result["prior"] = dpg.is_item_shown("prior_session_indicator") if dpg.does_item_exist("prior_session_indicator") else False
|
||||||
|
finally:
|
||||||
|
event.set()
|
||||||
|
|
||||||
|
with app._pending_gui_tasks_lock:
|
||||||
|
app._pending_gui_tasks.append({
|
||||||
|
"action": "custom_callback",
|
||||||
|
"callback": check_all
|
||||||
|
})
|
||||||
|
|
||||||
|
if event.wait(timeout=2):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(result).encode('utf-8'))
|
||||||
|
else:
|
||||||
|
self.send_response(504)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8'))
|
||||||
else:
|
else:
|
||||||
self.send_response(404)
|
self.send_response(404)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -70,11 +100,6 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
json.dumps({'status': 'updated'}).encode('utf-8'))
|
json.dumps({'status': 'updated'}).encode('utf-8'))
|
||||||
elif self.path == '/api/gui':
|
elif self.path == '/api/gui':
|
||||||
if not hasattr(app, '_pending_gui_tasks'):
|
|
||||||
app._pending_gui_tasks = []
|
|
||||||
if not hasattr(app, '_pending_gui_tasks_lock'):
|
|
||||||
app._pending_gui_tasks_lock = threading.Lock()
|
|
||||||
|
|
||||||
with app._pending_gui_tasks_lock:
|
with app._pending_gui_tasks_lock:
|
||||||
app._pending_gui_tasks.append(data)
|
app._pending_gui_tasks.append(data)
|
||||||
|
|
||||||
@@ -105,6 +130,13 @@ class HookServer:
|
|||||||
def start(self):
|
def start(self):
|
||||||
if not getattr(self.app, 'test_hooks_enabled', False):
|
if not getattr(self.app, 'test_hooks_enabled', False):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Ensure the app has the task queue and lock initialized
|
||||||
|
if not hasattr(self.app, '_pending_gui_tasks'):
|
||||||
|
self.app._pending_gui_tasks = []
|
||||||
|
if not hasattr(self.app, '_pending_gui_tasks_lock'):
|
||||||
|
self.app._pending_gui_tasks_lock = threading.Lock()
|
||||||
|
|
||||||
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
|
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
|
||||||
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Implementation Plan: API Usage Audit and Alignment
|
||||||
|
|
||||||
|
## Phase 1: Research and Comprehensive Audit [checkpoint: 5ec4283]
|
||||||
|
Identify all points of interaction with AI SDKs and compare them with latest official documentation.
|
||||||
|
|
||||||
|
- [x] Task: List and categorize all AI SDK usage in the project.
|
||||||
|
- [x] Search for all imports of `google.genai` and `anthropic`.
|
||||||
|
- [x] Document specific functions and methods being called.
|
||||||
|
- [x] Task: Research latest official documentation for `google-genai` and `anthropic` Python SDKs.
|
||||||
|
- [x] Verify latest patterns for Client initialization.
|
||||||
|
- [x] Verify latest patterns for Context/Prompt caching.
|
||||||
|
- [x] Verify latest patterns for Tool/Function calling.
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Research and Comprehensive Audit' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase 2: Gemini (google-genai) Alignment [checkpoint: 842bfc4]
|
||||||
|
Align Gemini integration with documented best practices.
|
||||||
|
|
||||||
|
- [x] Task: Refactor Gemini Client and Chat initialization if needed.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Optimize Gemini Context Caching.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Align Gemini Tool Declaration and handling.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Gemini (google-genai) Alignment' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase 3: Anthropic Alignment [checkpoint: f0eb538]
|
||||||
|
Align Anthropic integration with documented best practices.
|
||||||
|
|
||||||
|
- [x] Task: Refactor Anthropic Client and Message creation if needed.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Optimize Anthropic Prompt Caching (`cache_control`).
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Align Anthropic Tool Declaration and handling.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Anthropic Alignment' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase 4: History and Token Management [checkpoint: 0f9f235]
|
||||||
|
Ensure accurate token estimation and robust history handling.
|
||||||
|
|
||||||
|
- [x] Task: Review and align token estimation logic for both providers.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Audit message history truncation and context window management.
|
||||||
|
- [x] Write Tests
|
||||||
|
- [x] Implement Feature
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: History and Token Management' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase 5: Final Validation and Cleanup [checkpoint: e9126b4]
|
||||||
|
- [x] Task: Perform a full test run using `run_tests.py` to ensure 100% pass rate.
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Validation and Cleanup' (Protocol in workflow.md)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# GUI Layout Audit Report
|
||||||
|
|
||||||
|
## Current Panel Distribution
|
||||||
|
The GUI currently uses a multi-column layout with hardcoded initial positions:
|
||||||
|
|
||||||
|
1. **Column 1 (Left):** Projects (Top), Files (Mid), Diagnostics (Bottom).
|
||||||
|
2. **Column 2 (Center-Left):** Screenshots (Top), Theme (Mid), System Prompts (Bottom).
|
||||||
|
3. **Column 3 (Center-Right):** Discussion History (Full Height).
|
||||||
|
4. **Column 4 (Right):** Provider (Top), Message (Mid-Top), Response (Mid-Bottom), Tool Calls (Bottom).
|
||||||
|
5. **Column 5 (Far-Right):** Comms History (Full Height).
|
||||||
|
|
||||||
|
## Identified Issues
|
||||||
|
|
||||||
|
### 1. Context Fragmentation
|
||||||
|
- **Projects**, **Files**, and **Screenshots** are related to context gathering but are split across two different columns.
|
||||||
|
- **Base Dir** inputs are repeated for Files and Screenshots, taking up redundant vertical space.
|
||||||
|
|
||||||
|
### 2. Configuration Fragmentation
|
||||||
|
- **Provider** settings (API keys, models, temperature) are on the far right.
|
||||||
|
- **System Prompts** (Global and Project) are in the center-bottom.
|
||||||
|
- These should be unified into a single "AI Configuration" or "Settings" hub.
|
||||||
|
|
||||||
|
### 3. Workflow Disconnect (The "Chat Loop")
|
||||||
|
- The user composes in **Message**, views in **Response**, and then manually adds to **Discussion History**.
|
||||||
|
- These three panels are physically separated (Column 3 vs Column 4), causing unnecessary eye travel.
|
||||||
|
|
||||||
|
### 4. Visibility of Operations
|
||||||
|
- **Diagnostics** and **Comms History** are related to monitoring "under the hood" activity but are at opposite ends of the screen (Far Left vs Far Right).
|
||||||
|
- **Tool Calls** and **Last Script Output** are the primary way to see AI actions, but Tool Calls is small and Script Output is a popup that can be missed.
|
||||||
|
|
||||||
|
### 5. Tactical UI Density
|
||||||
|
- Heavy use of `dpg.add_separator()` and standard `dpg.add_text()` labels leads to "airy" panels that don't match the "Arcade" aesthetic of dense, information-rich displays.
|
||||||
|
- Lack of clear visual grouping for related fields.
|
||||||
|
|
||||||
|
## Recommendations for Phase 2
|
||||||
|
- **Unify Context:** Merge Projects, Files, and Screenshots into a tabbed "Context Manager" panel.
|
||||||
|
- **Unify AI Config:** Merge Provider and System Prompts into an "AI Settings" panel.
|
||||||
|
- **Streamline Chat:** Position Discussion History, Message, and Response in a logical vertical or horizontal flow.
|
||||||
|
- **Operations Hub:** Group Diagnostics, Comms History, and Tool Calls.
|
||||||
|
- **Arcade FX:** Implement better visual cues (blinking, color shifts) for state changes.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Implementation Plan: GUI Layout Audit and UX Refinement
|
||||||
|
|
||||||
|
## Phase 1: Audit and Structural Design [checkpoint: 6a35da1]
|
||||||
|
Perform a thorough review of the current GUI and define the target layout.
|
||||||
|
|
||||||
|
- [x] Task: Audit current GUI panels (AI Settings, Context, Diagnostics, History) and document placement issues. d177c0b
|
||||||
|
- [x] Task: Propose a reorganized layout structure that prioritizes dockable/floatable window flexibility. 8448c71
|
||||||
|
- [x] Task: Review proposal with user and finalize the structural plan. 8448c71
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Audit and Structural Design' (Protocol in workflow.md) 6a35da1
|
||||||
|
|
||||||
|
## Phase 2: Layout Reorganization [checkpoint: 97367fe]
|
||||||
|
Implement the structural changes to panel placements and window behaviors.
|
||||||
|
|
||||||
|
- [x] Task: Refactor `gui.py` panel definitions to align with the new structural plan. c341de5
|
||||||
|
- [x] Task: Optimize Dear PyGui window configuration for better multi-viewport handling. f8fb58d
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Layout Reorganization' (Protocol in workflow.md) 97367fe
|
||||||
|
|
||||||
|
## Phase 3: Visual and Tactile Enhancements [checkpoint: 4a4cf8c]
|
||||||
|
Implement Arcade FX and increase information density.
|
||||||
|
|
||||||
|
- [x] Task: Enhance Arcade FX (blinking, animations) for AI state changes and tool execution. c5d54cf
|
||||||
|
- [x] Task: Increase tactile density in diagnostic and context tables. c5d54cf
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Visual and Tactile Enhancements' (Protocol in workflow.md) 4a4cf8c
|
||||||
|
|
||||||
|
## Phase 4: Iterative Refinement and Final Audit [checkpoint: 22f8943]
|
||||||
|
Fine-tune the UI based on live usage and verify against product guidelines.
|
||||||
|
|
||||||
|
- [x] Task: Perform a "live" walkthrough to identify friction points in the new layout. b3cf58a
|
||||||
|
- [x] Task: Final polish of widget spacing, colors, and tactile feedback based on walkthrough. ebd8158
|
||||||
|
- [x] Task: Revert Diagnostics to standalone panel and increase plot height. ebd8158
|
||||||
|
- [x] Task: Update Discussion Entries (collapsed by default, read-only mode toggle). ebd8158
|
||||||
|
- [x] Task: Reposition Maximize button (away from insert/delete). ebd8158
|
||||||
|
- [x] Task: Implement Message/Response as tabs. ebd8158
|
||||||
|
- [x] Task: Ensure all read-only text is selectable/copyable. ebd8158
|
||||||
|
- [x] Task: Implement "Prior Session Log" viewer with tinted UI mode. ebd8158
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Iterative Refinement and Final Audit' (Protocol in workflow.md) 22f8943
|
||||||
|
|
||||||
|
## Phase: Review Fixes
|
||||||
|
- [x] Task: Apply review suggestions (Align diagnostics test) 0c5ac55
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# GUI Reorganization Proposal: The "Integrated Workspace"
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
Transform the current scattered window layout into a cohesive, professional workspace that optimizes expert-level AI interaction. We will group functionality into four primary dockable "Hubs" while maintaining the flexibility of floating windows for secondary tasks.
|
||||||
|
|
||||||
|
## 1. Context Hub (The "Input" Panel)
|
||||||
|
**Goal:** Consolidate all files, projects, and assets.
|
||||||
|
- **Components:**
|
||||||
|
- Tab 1: **Projects** (Project switching, global settings).
|
||||||
|
- Tab 2: **Files** (Base directory, path list, wildcard tools).
|
||||||
|
- Tab 3: **Screenshots** (Base directory, path list, preview).
|
||||||
|
- **Benefits:** Reduces eye-scatter when gathering context; shared vertical space for lists.
|
||||||
|
|
||||||
|
## 2. AI Settings Hub (The "Brain" Panel)
|
||||||
|
**Goal:** Unified control over AI persona and parameters.
|
||||||
|
- **Components:**
|
||||||
|
- Section (Collapsing): **Provider & Models** (Provider selection, model fetcher, telemetry).
|
||||||
|
- Section (Collapsing): **Tunings** (Temperature, Max Tokens, Truncation Limit).
|
||||||
|
- Section (Collapsing): **System Prompts** (Global and Project-specific overrides).
|
||||||
|
- **Benefits:** All "static" AI configuration in one place, freeing up right-column space for the chat flow.
|
||||||
|
|
||||||
|
## 3. Discussion Hub (The "Interface" Panel)
|
||||||
|
**Goal:** A tight feedback loop for the core chat experience.
|
||||||
|
- **Layout:**
|
||||||
|
- **Top:** Discussion History (Scrollable region).
|
||||||
|
- **Middle:** Message Composer (Input box + "Gen + Send" buttons).
|
||||||
|
- **Bottom:** AI Response (Read-only output with "-> History" action).
|
||||||
|
- **Benefits:** Minimizes mouse travel between input, output, and history archival. Supports a natural top-to-bottom reading flow.
|
||||||
|
|
||||||
|
## 4. Operations Hub (The "Diagnostics" Panel)
|
||||||
|
**Goal:** High-density monitoring of background activity.
|
||||||
|
- **Components:**
|
||||||
|
- Tab 1: **Comms History** (The low-level request/response log).
|
||||||
|
- Tab 2: **Tool Log** (Specific record of executed tools and scripts).
|
||||||
|
- Tab 3: **Diagnostics** (Performance telemetry, FPS/CPU plots).
|
||||||
|
- **Benefits:** Keeps "noisy" technical data out of the primary workspace while making it easily accessible for troubleshooting.
|
||||||
|
|
||||||
|
## Visual & Tactile Enhancements (Arcade FX)
|
||||||
|
- **State-Based Blinking:** Unified blinking logic for when the AI is "Thinking" vs "Ready".
|
||||||
|
- **Density:** Transition from simple separators to titled grouping boxes and compact tables for token usage.
|
||||||
|
- **Color Coding:** Standardized color palette for different tool types (Files = Blue, Shell = Yellow, Web = Green).
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
1. **Docking Defaults:** Define a default docking layout in `gui.py` that arranges these four Hubs in a 4-quadrant or 2x2 grid.
|
||||||
|
2. **Refactor:** Modify `gui.py` to wrap current window contents into these new Hub functions.
|
||||||
|
3. **Persistence:** Ensure `dpg_layout.ini` continues to respect user overrides for this new structure.
|
||||||
@@ -13,4 +13,7 @@ To serve as an expert-level utility for personal developer use on small projects
|
|||||||
- **Explicit Execution Control:** All AI-generated PowerShell scripts require explicit human confirmation via interactive UI dialogs before execution.
|
- **Explicit Execution Control:** All AI-generated PowerShell scripts require explicit human confirmation via interactive UI dialogs before execution.
|
||||||
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
||||||
- **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface.
|
- **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface.
|
||||||
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
|
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows.
|
||||||
|
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
|
||||||
|
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
|
||||||
|
- **Automated UX Verification:** A robust IPC mechanism via API hooks allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle.
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
- **tomli-w:** For writing TOML configuration files.
|
- **tomli-w:** For writing TOML configuration files.
|
||||||
- **psutil:** For system and process monitoring (CPU/Memory telemetry).
|
- **psutil:** For system and process monitoring (CPU/Memory telemetry).
|
||||||
- **uv:** An extremely fast Python package and project manager.
|
- **uv:** An extremely fast Python package and project manager.
|
||||||
|
- **pytest:** For unit and integration testing, leveraging custom fixtures for live GUI verification.
|
||||||
|
- **ApiHookClient:** A dedicated IPC client for automated GUI interaction and state inspection.
|
||||||
|
|
||||||
## Architectural Patterns
|
## Architectural Patterns
|
||||||
- **Event-Driven Metrics:** Uses a custom `EventEmitter` to decouple API lifecycle events from UI rendering, improving performance and responsiveness.
|
- **Event-Driven Metrics:** Uses a custom `EventEmitter` to decouple API lifecycle events from UI rendering, improving performance and responsiveness.
|
||||||
+4
-7
@@ -9,13 +9,10 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] **Track: Review project codebase, documentation related to project, and make sure agenti vendor apis are being used as properly stated by offical documentation from google for gemini and anthropic for claude.**
|
- [x] **Track: Make a human-like test ux interaction where the AI creates a small python project, engages in a 5-turn discussion, and verifies history/session management features via API hooks.**
|
||||||
*Link: [./tracks/api_vendor_alignment_20260223/](./tracks/api_vendor_alignment_20260223/)*
|
*Link: [./tracks/live_ux_test_20260223/](./tracks/live_ux_test_20260223/)*
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- [ ] **Track: Review GUI design. Make sure placment of tunings, features, etc that the gui provides frontend visualization and manipulation for make sense and are in the right place (not in a weird panel or doesn't make sense holistically for its use. Make plan for adjustments and then make major changes to meet resolved goals.**
|
|
||||||
*Link: [./tracks/gui_layout_refinement_20260223/](./tracks/gui_layout_refinement_20260223/)*
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# Implementation Plan: API Usage Audit and Alignment
|
|
||||||
|
|
||||||
## Phase 1: Research and Comprehensive Audit
|
|
||||||
Identify all points of interaction with AI SDKs and compare them with latest official documentation.
|
|
||||||
|
|
||||||
- [ ] Task: List and categorize all AI SDK usage in the project.
|
|
||||||
- [ ] Search for all imports of `google.genai` and `anthropic`.
|
|
||||||
- [ ] Document specific functions and methods being called.
|
|
||||||
- [ ] Task: Research latest official documentation for `google-genai` and `anthropic` Python SDKs.
|
|
||||||
- [ ] Verify latest patterns for Client initialization.
|
|
||||||
- [ ] Verify latest patterns for Context/Prompt caching.
|
|
||||||
- [ ] Verify latest patterns for Tool/Function calling.
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Research and Comprehensive Audit' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 2: Gemini (google-genai) Alignment
|
|
||||||
Align Gemini integration with documented best practices.
|
|
||||||
|
|
||||||
- [ ] Task: Refactor Gemini Client and Chat initialization if needed.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Optimize Gemini Context Caching.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Align Gemini Tool Declaration and handling.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Gemini (google-genai) Alignment' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 3: Anthropic Alignment
|
|
||||||
Align Anthropic integration with documented best practices.
|
|
||||||
|
|
||||||
- [ ] Task: Refactor Anthropic Client and Message creation if needed.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Optimize Anthropic Prompt Caching (`cache_control`).
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Align Anthropic Tool Declaration and handling.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Anthropic Alignment' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 4: History and Token Management
|
|
||||||
Ensure accurate token estimation and robust history handling.
|
|
||||||
|
|
||||||
- [ ] Task: Review and align token estimation logic for both providers.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Audit message history truncation and context window management.
|
|
||||||
- [ ] Write Tests
|
|
||||||
- [ ] Implement Feature
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: History and Token Management' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 5: Final Validation and Cleanup
|
|
||||||
- [ ] Task: Perform a full test run using `run_tests.py` to ensure 100% pass rate.
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Final Validation and Cleanup' (Protocol in workflow.md)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Implementation Plan: GUI Layout Audit and UX Refinement
|
|
||||||
|
|
||||||
## Phase 1: Audit and Structural Design
|
|
||||||
Perform a thorough review of the current GUI and define the target layout.
|
|
||||||
|
|
||||||
- [ ] Task: Audit current GUI panels (AI Settings, Context, Diagnostics, History) and document placement issues.
|
|
||||||
- [ ] Task: Propose a reorganized layout structure that prioritizes dockable/floatable window flexibility.
|
|
||||||
- [ ] Task: Review proposal with user and finalize the structural plan.
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Audit and Structural Design' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 2: Layout Reorganization
|
|
||||||
Implement the structural changes to panel placements and window behaviors.
|
|
||||||
|
|
||||||
- [ ] Task: Refactor `gui.py` panel definitions to align with the new structural plan.
|
|
||||||
- [ ] Write Tests (Verify panel existence and dockable flags)
|
|
||||||
- [ ] Implement layout changes
|
|
||||||
- [ ] Task: Optimize Dear PyGui window configuration for better multi-viewport handling.
|
|
||||||
- [ ] Write Tests (Verify window persistence/flags)
|
|
||||||
- [ ] Implement changes
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Layout Reorganization' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 3: Visual and Tactile Enhancements
|
|
||||||
Implement Arcade FX and increase information density.
|
|
||||||
|
|
||||||
- [ ] Task: Enhance Arcade FX (blinking, animations) for AI state changes and tool execution.
|
|
||||||
- [ ] Write Tests (Simulate state changes)
|
|
||||||
- [ ] Implement FX logic in `gui.py`
|
|
||||||
- [ ] Task: Increase tactile density in diagnostic and context tables.
|
|
||||||
- [ ] Write Tests (Check widget count/spacing in specific panels)
|
|
||||||
- [ ] Implement density improvements
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Visual and Tactile Enhancements' (Protocol in workflow.md)
|
|
||||||
|
|
||||||
## Phase 4: Iterative Refinement and Final Audit
|
|
||||||
Fine-tune the UI based on live usage and verify against product guidelines.
|
|
||||||
|
|
||||||
- [ ] Task: Perform a "live" walkthrough to identify friction points in the new layout.
|
|
||||||
- [ ] Task: Final polish of widget spacing, colors, and tactile feedback.
|
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Iterative Refinement and Final Audit' (Protocol in workflow.md)
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Track live_ux_test_20260223 Context
|
||||||
|
|
||||||
|
- [Specification](./spec.md)
|
||||||
|
- [Implementation Plan](./plan.md)
|
||||||
|
- [Metadata](./metadata.json)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"track_id": "live_ux_test_20260223",
|
||||||
|
"type": "feature",
|
||||||
|
"status": "new",
|
||||||
|
"created_at": "2026-02-23T19:14:00Z",
|
||||||
|
"updated_at": "2026-02-23T19:14:00Z",
|
||||||
|
"description": "Make a human-like test ux interaction where the AI creates a small python project, engages in a 5-turn discussion, and verifies history/session management features via API hooks."
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Implementation Plan: Human-Like UX Interaction Test
|
||||||
|
|
||||||
|
## Phase 1: Infrastructure & Automation Core [checkpoint: 7626531]
|
||||||
|
Establish the foundation for driving the GUI via API hooks and simulation logic.
|
||||||
|
|
||||||
|
- [x] Task: Extend `ApiHookClient` with methods for tab switching and listbox selection if missing. f36d539
|
||||||
|
- [x] Task: Implement `TestUserAgent` class to manage dynamic response generation and action delays. d326242
|
||||||
|
- [x] Task: Write Tests (Verify basic hook connectivity and simulated delays) f36d539
|
||||||
|
- [x] Task: Implement basic 'ping-pong' interaction via hooks. bfe9ef0
|
||||||
|
- [x] Task: Harden API hook thread-safety and simplify GUI state polling. 8bd280e
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Automation Core' (Protocol in workflow.md) 7626531
|
||||||
|
|
||||||
|
## Phase 2: Workflow Simulation [checkpoint: 9c4a72c]
|
||||||
|
Build the core interaction loop for project creation and AI discussion.
|
||||||
|
|
||||||
|
- [x] Task: Implement 'New Project' scaffolding script (creating a tiny console program). bd5dc16
|
||||||
|
- [x] Task: Implement 5-turn discussion loop logic with sub-agent responses. bd5dc16
|
||||||
|
- [x] Task: Write Tests (Verify state changes in Discussion Hub during simulated chat) 6d16438
|
||||||
|
- [x] Task: Implement 'Thinking' and 'Live' indicator verification logic. 6d16438
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Workflow Simulation' (Protocol in workflow.md) 9c4a72c
|
||||||
|
|
||||||
|
## Phase 3: History & Session Verification [checkpoint: 0f04e06]
|
||||||
|
Simulate complex session management and historical audit features.
|
||||||
|
|
||||||
|
- [x] Task: Implement discussion switching logic (creating/switching between named discussions). 5e1b965
|
||||||
|
- [x] Task: Implement 'Load Prior Log' simulation and 'Tinted Mode' detection. 5e1b965
|
||||||
|
- [x] Task: Write Tests (Verify log loading and tab navigation consistency) 5e1b965
|
||||||
|
- [x] Task: Implement truncation limit verification (forcing a long history and checking bleed). 5e1b965
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: History & Session Verification' (Protocol in workflow.md) 0f04e06
|
||||||
|
|
||||||
|
## Phase 4: Final Integration & Regression [checkpoint: 8e63b31]
|
||||||
|
Consolidate the simulation into end-user artifacts and CI tests.
|
||||||
|
|
||||||
|
- [x] Task: Create `live_walkthrough.py` with full visual feedback and manual sign-off. 8bd280e
|
||||||
|
- [x] Task: Create `tests/test_live_workflow.py` for automated regression testing. 8bd280e
|
||||||
|
- [x] Task: Perform a full visual walkthrough and verify 'human-readable' pace. 8e63b31
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Regression' (Protocol in workflow.md) 8e63b31
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Specification: Human-Like UX Interaction Test
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This track implements a robust, "human-like" interaction test suite for Manual Slop. The suite will simulate a real user's workflow—from project creation to complex AI discussions and history management—using the application's API hooks. It aims to verify the "Integrated Workspace" functionality, tool execution, and history persistence without requiring manual human input, while remaining slow enough for visual audit.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- **Standalone Interactive Test**: A Python script (`live_walkthrough.py`) that drives the GUI through a full session, ending with an optional manual sign-off.
|
||||||
|
- **Automated Regression Test**: A pytest integration (`tests/test_live_workflow.py`) that executes the same logic in a headless or automated fashion for CI.
|
||||||
|
- **Target Model**: Google Gemini Flash 2.5.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
1. **User Simulation**:
|
||||||
|
- **Dynamic Messaging**: The test agent will generate responses based on the AI's output to simulate a multi-turn conversation.
|
||||||
|
- **Tactile Delays**: Short, random delays (minimum 0.5s) between actions to simulate reading and "typing" time.
|
||||||
|
- **Visual Feedback**: Automatic scrolling of the discussion history and comms logs to keep the "live" action in view.
|
||||||
|
2. **Workflow Scenarios**:
|
||||||
|
- **Project Scaffolding**: Create a new project and initialize a tiny console-based Python program.
|
||||||
|
- **Discussion Loop**: Engage in a ~5-turn conversation with the AI to refine the code.
|
||||||
|
- **Context Management**: Verify that tool calls (filesystem, shell) are reflected correctly in the Comms and Tool Log tabs.
|
||||||
|
- **History Depth**: Verify truncation limits and switching between named discussions.
|
||||||
|
3. **Session Management**:
|
||||||
|
- **Tab Interaction**: Programmatically switch between "Comms Log" and "Tool Log" tabs during operations.
|
||||||
|
- **Historical Audit**: Use the "Load Session Log" feature to load a prior log file and verify "Tinted Mode" visibility.
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
- **Efficiency**: Minimize token usage by using Gemini Flash and keeping the "User" prompts concise.
|
||||||
|
- **Observability**: The standalone test must be clearly visible to a human observer, with state changes occurring at a "human-readable" pace.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
- `live_walkthrough.py` successfully completes a 5-turn discussion and signs off.
|
||||||
|
- `tests/test_live_workflow.py` passes in CI environment.
|
||||||
|
- Prior session logs are loaded and visualized without crashing.
|
||||||
|
- Thinking and Live indicators trigger correctly during simulated API calls.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- Support for Anthropic API in this specific test track.
|
||||||
|
- Stress testing high-concurrency tool calls.
|
||||||
+2
-1
@@ -16,5 +16,6 @@ scale = 1.0
|
|||||||
paths = [
|
paths = [
|
||||||
"manual_slop.toml",
|
"manual_slop.toml",
|
||||||
"C:/projects/forth/bootslop/bootslop.toml",
|
"C:/projects/forth/bootslop/bootslop.toml",
|
||||||
|
"C:\\projects\\manual_slop\\tests\\temp_project.toml",
|
||||||
]
|
]
|
||||||
active = "manual_slop.toml"
|
active = "C:\\projects\\manual_slop\\tests\\temp_project.toml"
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ class App:
|
|||||||
self.current_provider: str = ai_cfg.get("provider", "gemini")
|
self.current_provider: str = ai_cfg.get("provider", "gemini")
|
||||||
self.current_model: str = ai_cfg.get("model", "gemini-2.0-flash")
|
self.current_model: str = ai_cfg.get("model", "gemini-2.0-flash")
|
||||||
self.available_models: list[str] = []
|
self.available_models: list[str] = []
|
||||||
|
self.temperature: float = ai_cfg.get("temperature", 0.0)
|
||||||
|
self.max_tokens: int = ai_cfg.get("max_tokens", 8192)
|
||||||
|
self.history_trunc_limit: int = ai_cfg.get("history_trunc_limit", 8000)
|
||||||
|
|
||||||
projects_cfg = self.config.get("projects", {})
|
projects_cfg = self.config.get("projects", {})
|
||||||
self.project_paths: list[str] = list(projects_cfg.get("paths", []))
|
self.project_paths: list[str] = list(projects_cfg.get("paths", []))
|
||||||
@@ -176,6 +179,8 @@ class App:
|
|||||||
self._is_script_blinking = False
|
self._is_script_blinking = False
|
||||||
self._script_blink_start_time = 0.0
|
self._script_blink_start_time = 0.0
|
||||||
|
|
||||||
|
self._scroll_disc_to_bottom = False
|
||||||
|
|
||||||
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
|
||||||
@@ -376,7 +381,13 @@ class App:
|
|||||||
disc_sec["auto_add"] = self.ui_auto_add_history
|
disc_sec["auto_add"] = self.ui_auto_add_history
|
||||||
|
|
||||||
def _flush_to_config(self):
|
def _flush_to_config(self):
|
||||||
self.config["ai"] = {"provider": self.current_provider, "model": self.current_model}
|
self.config["ai"] = {
|
||||||
|
"provider": self.current_provider,
|
||||||
|
"model": self.current_model,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
|
"history_trunc_limit": self.history_trunc_limit,
|
||||||
|
}
|
||||||
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}
|
||||||
theme.save_to_config(self.config)
|
theme.save_to_config(self.config)
|
||||||
@@ -441,6 +452,8 @@ class App:
|
|||||||
self._pending_comms.clear()
|
self._pending_comms.clear()
|
||||||
|
|
||||||
with self._pending_history_adds_lock:
|
with self._pending_history_adds_lock:
|
||||||
|
if self._pending_history_adds:
|
||||||
|
self._scroll_disc_to_bottom = True
|
||||||
for item in self._pending_history_adds:
|
for item in self._pending_history_adds:
|
||||||
if item["role"] not in self.disc_roles:
|
if item["role"] not in self.disc_roles:
|
||||||
self.disc_roles.append(item["role"])
|
self.disc_roles.append(item["role"])
|
||||||
@@ -453,22 +466,22 @@ class App:
|
|||||||
_, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w])
|
_, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w])
|
||||||
imgui.end_menu()
|
imgui.end_menu()
|
||||||
if imgui.begin_menu("Project"):
|
if imgui.begin_menu("Project"):
|
||||||
if imgui.menu_item("Save All")[0]:
|
if imgui.menu_item("Save All", "", False)[0]:
|
||||||
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)
|
||||||
self.ai_status = "config saved"
|
self.ai_status = "config saved"
|
||||||
if imgui.menu_item("Reset Session")[0]:
|
if imgui.menu_item("Reset Session", "", False)[0]:
|
||||||
ai_client.reset_session()
|
ai_client.reset_session()
|
||||||
ai_client.clear_comms_log()
|
ai_client.clear_comms_log()
|
||||||
self._tool_log.clear()
|
self._tool_log.clear()
|
||||||
self._comms_log.clear()
|
self._comms_log.clear()
|
||||||
self.ai_status = "session reset"
|
self.ai_status = "session reset"
|
||||||
self.ai_response = ""
|
self.ai_response = ""
|
||||||
if imgui.menu_item("Generate MD Only")[0]:
|
if imgui.menu_item("Generate MD Only", "", False)[0]:
|
||||||
try:
|
try:
|
||||||
md, path, _ = self._do_generate()
|
md, path, *_ = self._do_generate()
|
||||||
self.last_md = md
|
self.last_md = md
|
||||||
self.last_md_path = path
|
self.last_md_path = path
|
||||||
self.ai_status = f"md written: {path.name}"
|
self.ai_status = f"md written: {path.name}"
|
||||||
@@ -535,7 +548,10 @@ class App:
|
|||||||
|
|
||||||
if imgui.button("Add Project"):
|
if imgui.button("Add Project"):
|
||||||
r = hide_tk_root()
|
r = hide_tk_root()
|
||||||
p = filedialog.askopenfilename(title="Select Project .toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")])
|
p = filedialog.askopenfilename(
|
||||||
|
title="Select Project .toml",
|
||||||
|
filetypes=[("TOML", "*.toml"), ("All", "*.*")],
|
||||||
|
)
|
||||||
r.destroy()
|
r.destroy()
|
||||||
if p and p not in self.project_paths:
|
if p and p not in self.project_paths:
|
||||||
self.project_paths.append(p)
|
self.project_paths.append(p)
|
||||||
@@ -626,7 +642,10 @@ class App:
|
|||||||
|
|
||||||
if imgui.button("Add Screenshot(s)"):
|
if imgui.button("Add Screenshot(s)"):
|
||||||
r = hide_tk_root()
|
r = hide_tk_root()
|
||||||
paths = filedialog.askopenfilenames()
|
paths = filedialog.askopenfilenames(
|
||||||
|
title="Select Screenshots",
|
||||||
|
filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")],
|
||||||
|
)
|
||||||
r.destroy()
|
r.destroy()
|
||||||
for p in paths:
|
for p in paths:
|
||||||
if p not in self.screenshots: self.screenshots.append(p)
|
if p not in self.screenshots: self.screenshots.append(p)
|
||||||
@@ -779,6 +798,9 @@ class App:
|
|||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
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_child()
|
||||||
imgui.end()
|
imgui.end()
|
||||||
|
|
||||||
@@ -809,6 +831,11 @@ class App:
|
|||||||
ai_client.reset_session()
|
ai_client.reset_session()
|
||||||
ai_client.set_provider(self.current_provider, m)
|
ai_client.set_provider(self.current_provider, m)
|
||||||
imgui.end_list_box()
|
imgui.end_list_box()
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text("Parameters")
|
||||||
|
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.end()
|
imgui.end()
|
||||||
|
|
||||||
# ---- Message
|
# ---- Message
|
||||||
@@ -820,7 +847,7 @@ class App:
|
|||||||
if imgui.button("Gen + Send"):
|
if imgui.button("Gen + Send"):
|
||||||
if not (self.send_thread and self.send_thread.is_alive()):
|
if not (self.send_thread and self.send_thread.is_alive()):
|
||||||
try:
|
try:
|
||||||
md, path, file_items = self._do_generate()
|
md, path, file_items, stable_md, disc_text = self._do_generate()
|
||||||
self.last_md = md
|
self.last_md = md
|
||||||
self.last_md_path = path
|
self.last_md_path = path
|
||||||
self.last_file_items = file_items
|
self.last_file_items = file_items
|
||||||
@@ -833,6 +860,7 @@ class App:
|
|||||||
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))
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -865,7 +893,7 @@ class App:
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("MD Only"):
|
if imgui.button("MD Only"):
|
||||||
try:
|
try:
|
||||||
md, path, _ = self._do_generate()
|
md, path, *_ = self._do_generate()
|
||||||
self.last_md = md
|
self.last_md = md
|
||||||
self.last_md_path = path
|
self.last_md_path = path
|
||||||
self.ai_status = f"md written: {path.name}"
|
self.ai_status = f"md written: {path.name}"
|
||||||
@@ -1247,22 +1275,29 @@ class App:
|
|||||||
if font_path and Path(font_path).exists():
|
if font_path and Path(font_path).exists():
|
||||||
hello_imgui.load_font(font_path, font_size)
|
hello_imgui.load_font(font_path, font_size)
|
||||||
|
|
||||||
|
def _post_init(self):
|
||||||
|
theme.apply_current()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
theme.load_from_config(self.config)
|
theme.load_from_config(self.config)
|
||||||
|
|
||||||
self.runner_params = hello_imgui.RunnerParams()
|
self.runner_params = hello_imgui.RunnerParams()
|
||||||
self.runner_params.app_window_params.window_title = "manual slop"
|
self.runner_params.app_window_params.window_title = "manual slop"
|
||||||
self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
|
self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
|
||||||
self.runner_params.imgui_window_params.enable_viewports = True
|
self.runner_params.imgui_window_params.enable_viewports = True
|
||||||
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
|
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
|
||||||
|
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||||
|
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||||
self.runner_params.callbacks.show_gui = self._gui_func
|
self.runner_params.callbacks.show_gui = self._gui_func
|
||||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||||
|
self.runner_params.callbacks.post_init = self._post_init
|
||||||
|
|
||||||
self._fetch_models(self.current_provider)
|
self._fetch_models(self.current_provider)
|
||||||
|
|
||||||
immapp.run(self.runner_params)
|
immapp.run(self.runner_params)
|
||||||
|
|
||||||
# On exit
|
# On exit
|
||||||
|
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()
|
||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
|
|||||||
+9
-2
@@ -26,7 +26,7 @@ roles = [
|
|||||||
"Vendor API",
|
"Vendor API",
|
||||||
"System",
|
"System",
|
||||||
]
|
]
|
||||||
active = "test for gemini conductor"
|
active = "test gemini mock interaction"
|
||||||
auto_add = true
|
auto_add = true
|
||||||
|
|
||||||
[discussion.discussions."Support files or mcp, or proper dynamic context uptake."]
|
[discussion.discussions."Support files or mcp, or proper dynamic context uptake."]
|
||||||
@@ -165,7 +165,7 @@ history = [
|
|||||||
|
|
||||||
[discussion.discussions."test for gemini conductor"]
|
[discussion.discussions."test for gemini conductor"]
|
||||||
git_commit = "ccdba69214aea169a4e707da7b425ae0c6139fca"
|
git_commit = "ccdba69214aea169a4e707da7b425ae0c6139fca"
|
||||||
last_updated = "2026-02-23T16:40:30"
|
last_updated = "2026-02-23T19:03:19"
|
||||||
history = [
|
history = [
|
||||||
"User:\nadded a file to yoru exposed files read it.",
|
"User:\nadded a file to yoru exposed files read it.",
|
||||||
"User:\nI have read the `MainContext.md` file. It contains extensive documentation about the \"Manual Slop\" project.\n\nWas `MainContext.md` the file you were referring to? If not, please specify the file's name or path.",
|
"User:\nI have read the `MainContext.md` file. It contains extensive documentation about the \"Manual Slop\" project.\n\nWas `MainContext.md` the file you were referring to? If not, please specify the file's name or path.",
|
||||||
@@ -229,8 +229,15 @@ history = [
|
|||||||
"@2026-02-23T16:19:29\nSystem:\n[PERFORMANCE ALERT] Frame time high: 158.2ms. Please consider optimizing recent changes or reducing load.",
|
"@2026-02-23T16:19:29\nSystem:\n[PERFORMANCE ALERT] Frame time high: 158.2ms. Please consider optimizing recent changes or reducing load.",
|
||||||
"@2026-02-23T16:19:59\nSystem:\n[PERFORMANCE ALERT] Frame time high: 221.8ms. Please consider optimizing recent changes or reducing load.",
|
"@2026-02-23T16:19:59\nSystem:\n[PERFORMANCE ALERT] Frame time high: 221.8ms. Please consider optimizing recent changes or reducing load.",
|
||||||
"@2026-02-23T16:29:49\nSystem:\n[PERFORMANCE ALERT] Frame time high: 795.2ms. Please consider optimizing recent changes or reducing load.",
|
"@2026-02-23T16:29:49\nSystem:\n[PERFORMANCE ALERT] Frame time high: 795.2ms. Please consider optimizing recent changes or reducing load.",
|
||||||
|
"@2026-02-23T18:57:49\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 123.4%. Please consider optimizing recent changes or reducing load.",
|
||||||
|
"@2026-02-23T18:58:19\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 109.4%. Please consider optimizing recent changes or reducing load.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[discussion.discussions."test gemini mock interaction"]
|
||||||
|
git_commit = ""
|
||||||
|
last_updated = "2026-02-23T19:29:52"
|
||||||
|
history = []
|
||||||
|
|
||||||
[agent.tools]
|
[agent.tools]
|
||||||
run_powershell = true
|
run_powershell = true
|
||||||
read_file = true
|
read_file = true
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
;;; !!! This configuration is handled by HelloImGui and stores several Ini Files, separated by markers like this:
|
||||||
|
;;;<<<INI_NAME>>>;;;
|
||||||
|
|
||||||
|
;;;<<<ImGui_655921752_Default>>>;;;
|
||||||
|
[Window][Debug##Default]
|
||||||
|
Pos=60,60
|
||||||
|
Size=400,400
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Projects]
|
||||||
|
Pos=209,396
|
||||||
|
Size=387,337
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000014,0
|
||||||
|
|
||||||
|
[Window][Files]
|
||||||
|
Pos=0,0
|
||||||
|
Size=207,1200
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000011,0
|
||||||
|
|
||||||
|
[Window][Screenshots]
|
||||||
|
Pos=209,0
|
||||||
|
Size=387,171
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000015,0
|
||||||
|
|
||||||
|
[Window][Discussion History]
|
||||||
|
Pos=598,128
|
||||||
|
Size=712,619
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x0000000E,0
|
||||||
|
|
||||||
|
[Window][Provider]
|
||||||
|
Pos=209,913
|
||||||
|
Size=387,287
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x0000000A,0
|
||||||
|
|
||||||
|
[Window][Message]
|
||||||
|
Pos=598,749
|
||||||
|
Size=712,451
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x0000000C,0
|
||||||
|
|
||||||
|
[Window][Response]
|
||||||
|
Pos=209,735
|
||||||
|
Size=387,176
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000010,0
|
||||||
|
|
||||||
|
[Window][Tool Calls]
|
||||||
|
Pos=1312,733
|
||||||
|
Size=368,144
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000008,0
|
||||||
|
|
||||||
|
[Window][Comms History]
|
||||||
|
Pos=1312,879
|
||||||
|
Size=368,321
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000006,0
|
||||||
|
|
||||||
|
[Window][System Prompts]
|
||||||
|
Pos=1312,0
|
||||||
|
Size=368,731
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000007,0
|
||||||
|
|
||||||
|
[Window][Theme]
|
||||||
|
Pos=209,173
|
||||||
|
Size=387,221
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000016,0
|
||||||
|
|
||||||
|
[Window][Text Viewer - Entry #7]
|
||||||
|
Pos=379,324
|
||||||
|
Size=900,700
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Docking][Data]
|
||||||
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=138,161 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=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
|
||||||
|
DockNode ID=0x00000013 Parent=0x0000000F SizeRef=405,394 Split=Y Selected=0x8CA2375C
|
||||||
|
DockNode ID=0x00000015 Parent=0x00000013 SizeRef=405,171 Selected=0xDF822E02
|
||||||
|
DockNode ID=0x00000016 Parent=0x00000013 SizeRef=405,221 Selected=0x8CA2375C
|
||||||
|
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=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=0x00000005 Parent=0x00000004 SizeRef=261,877 Split=Y Selected=0xDD6419BC
|
||||||
|
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=261,731 Selected=0xDD6419BC
|
||||||
|
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=261,144 Selected=0x1D56B311
|
||||||
|
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=261,321 Selected=0x8B4EBFA6
|
||||||
|
|
||||||
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
[Layout]
|
||||||
|
Name=Default
|
||||||
|
[StatusBar]
|
||||||
|
Show=false
|
||||||
|
ShowFps=true
|
||||||
|
[Theme]
|
||||||
|
Name=DarculaDarker
|
||||||
|
;;;<<<SplitIds>>>;;;
|
||||||
|
{"gImGuiSplitIDs":{"MainDockSpace":2949142533}}
|
||||||
+1
-1
@@ -35,5 +35,5 @@ active = "main"
|
|||||||
|
|
||||||
[discussion.discussions.main]
|
[discussion.discussions.main]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-02-23T16:52:30"
|
last_updated = "2026-02-23T19:01:39"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from api_hook_client import ApiHookClient
|
||||||
|
from simulation.workflow_sim import WorkflowSimulator
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = ApiHookClient()
|
||||||
|
print("=== Manual Slop: Live UX Walkthrough ===")
|
||||||
|
print("Connecting to GUI...")
|
||||||
|
if not client.wait_for_server(timeout=10):
|
||||||
|
print("Error: Could not connect to GUI. Ensure it is running with --enable-test-hooks")
|
||||||
|
return
|
||||||
|
|
||||||
|
sim = WorkflowSimulator(client)
|
||||||
|
|
||||||
|
# 1. Start Clean
|
||||||
|
print("\n[Action] Resetting Session...")
|
||||||
|
client.click("btn_reset")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 2. Project Scaffolding
|
||||||
|
project_name = f"LiveTest_{int(time.time())}"
|
||||||
|
# Use actual project dir for realism
|
||||||
|
git_dir = os.path.abspath(".")
|
||||||
|
|
||||||
|
print(f"\n[Action] Scaffolding Project: {project_name}")
|
||||||
|
sim.setup_new_project(project_name, git_dir)
|
||||||
|
|
||||||
|
# Enable auto-add so results appear in history automatically
|
||||||
|
client.set_value("auto_add_history", True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 3. Discussion Loop (3 turns for speed, but logic supports more)
|
||||||
|
turns = [
|
||||||
|
"Hi! I want to create a simple python script called 'hello.py' that prints the current date and time. Can you write it for me?",
|
||||||
|
"That looks great. Can you also add a feature to print the name of the operating system?",
|
||||||
|
"Excellent. Now, please create a requirements.txt file with 'requests' in it."
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, msg in enumerate(turns):
|
||||||
|
print(f"\n--- Turn {i+1} ---")
|
||||||
|
|
||||||
|
# Switch to Comms Log to see the send
|
||||||
|
client.select_tab("operations_tabs", "tab_comms")
|
||||||
|
|
||||||
|
sim.run_discussion_turn(msg)
|
||||||
|
|
||||||
|
# Check thinking indicator
|
||||||
|
state = client.get_indicator_state("thinking_indicator")
|
||||||
|
if state.get('shown'):
|
||||||
|
print("[Status] Thinking indicator is visible.")
|
||||||
|
|
||||||
|
# Switch to Tool Log halfway through wait
|
||||||
|
time.sleep(2)
|
||||||
|
client.select_tab("operations_tabs", "tab_tool")
|
||||||
|
|
||||||
|
# Wait for AI response if not already finished
|
||||||
|
# (run_discussion_turn already waits, so we just observe)
|
||||||
|
|
||||||
|
# 4. History Management
|
||||||
|
print("\n[Action] Creating new discussion thread...")
|
||||||
|
sim.create_discussion("Refinement")
|
||||||
|
|
||||||
|
print("\n[Action] Switching back to Default...")
|
||||||
|
sim.switch_discussion("Default")
|
||||||
|
|
||||||
|
# 5. Manual Sign-off Simulation
|
||||||
|
print("\n=== Walkthrough Complete ===")
|
||||||
|
print("Please verify the following in the GUI:")
|
||||||
|
print("1. The project metadata reflects the new project.")
|
||||||
|
print("2. The discussion history contains the 3 turns.")
|
||||||
|
print("3. The 'Refinement' discussion exists in the list.")
|
||||||
|
print("\nWalkthrough finished successfully.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Ensure project root is in path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from api_hook_client import ApiHookClient
|
||||||
|
from simulation.user_agent import UserSimAgent
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = ApiHookClient()
|
||||||
|
print("Waiting for hook server...")
|
||||||
|
if not client.wait_for_server(timeout=5):
|
||||||
|
print("Hook server not found. Start GUI with --enable-test-hooks")
|
||||||
|
return
|
||||||
|
|
||||||
|
sim_agent = UserSimAgent(client)
|
||||||
|
|
||||||
|
# 1. Reset session to start clean
|
||||||
|
print("Resetting session...")
|
||||||
|
client.click("btn_reset")
|
||||||
|
time.sleep(2) # Give it time to clear
|
||||||
|
|
||||||
|
# 2. Initial message
|
||||||
|
initial_msg = "Hello! I want to create a simple python script that prints 'Hello World'. Can you help me?"
|
||||||
|
print(f"
|
||||||
|
[USER]: {initial_msg}")
|
||||||
|
client.set_value("ai_input", initial_msg)
|
||||||
|
client.click("btn_gen_send")
|
||||||
|
|
||||||
|
# 3. Wait for AI response
|
||||||
|
print("Waiting for AI response...", end="", flush=True)
|
||||||
|
last_entry_count = 0
|
||||||
|
for _ in range(60): # 60 seconds max
|
||||||
|
time.sleep(1)
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
session = client.get_session()
|
||||||
|
entries = session.get('session', {}).get('entries', [])
|
||||||
|
|
||||||
|
if len(entries) > last_entry_count:
|
||||||
|
# Something happened
|
||||||
|
last_entry = entries[-1]
|
||||||
|
if last_entry.get('role') == 'AI' and last_entry.get('content'):
|
||||||
|
print(f"
|
||||||
|
|
||||||
|
[AI]: {last_entry.get('content')[:100]}...")
|
||||||
|
print("
|
||||||
|
Ping-pong successful!")
|
||||||
|
return
|
||||||
|
last_entry_count = len(entries)
|
||||||
|
|
||||||
|
print("
|
||||||
|
Timeout waiting for AI response")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import time
|
||||||
|
import random
|
||||||
|
import ai_client
|
||||||
|
|
||||||
|
class UserSimAgent:
|
||||||
|
def __init__(self, hook_client, model="gemini-2.0-flash"):
|
||||||
|
self.hook_client = hook_client
|
||||||
|
self.model = model
|
||||||
|
self.system_prompt = (
|
||||||
|
"You are a software engineer testing an AI coding assistant called 'Manual Slop'. "
|
||||||
|
"You want to build a small Python project and verify the assistant's capabilities. "
|
||||||
|
"Keep your responses concise and human-like. "
|
||||||
|
"Do not use markdown blocks for your main message unless you are providing code."
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_response(self, conversation_history):
|
||||||
|
"""
|
||||||
|
Generates a human-like response based on the conversation history.
|
||||||
|
conversation_history: list of dicts with 'role' and 'content'
|
||||||
|
"""
|
||||||
|
# Format history for ai_client
|
||||||
|
# ai_client expects md_content and user_message.
|
||||||
|
# It handles its own internal history.
|
||||||
|
# We want the 'User AI' to have context of what the 'Assistant AI' said.
|
||||||
|
|
||||||
|
# For now, let's just use the last message from Assistant as the prompt.
|
||||||
|
last_ai_msg = ""
|
||||||
|
for entry in reversed(conversation_history):
|
||||||
|
if entry.get('role') == 'AI':
|
||||||
|
last_ai_msg = entry.get('content', '')
|
||||||
|
break
|
||||||
|
|
||||||
|
# We need to set a custom system prompt for the User Simulator
|
||||||
|
ai_client.set_custom_system_prompt(self.system_prompt)
|
||||||
|
|
||||||
|
# We'll use a blank md_content for now as the 'User' doesn't need to read its own files
|
||||||
|
# via the same mechanism, but we could provide it if needed.
|
||||||
|
response = ai_client.send(md_content="", user_message=last_ai_msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def perform_action_with_delay(self, action_func, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Executes an action with a human-like delay.
|
||||||
|
"""
|
||||||
|
delay = random.uniform(0.5, 2.0)
|
||||||
|
time.sleep(delay)
|
||||||
|
return action_func(*args, **kwargs)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import time
|
||||||
|
import os
|
||||||
|
from api_hook_client import ApiHookClient
|
||||||
|
from simulation.user_agent import UserSimAgent
|
||||||
|
|
||||||
|
class WorkflowSimulator:
|
||||||
|
def __init__(self, hook_client: ApiHookClient):
|
||||||
|
self.client = hook_client
|
||||||
|
self.user_agent = UserSimAgent(hook_client)
|
||||||
|
|
||||||
|
def setup_new_project(self, name, git_dir):
|
||||||
|
print(f"Setting up new project: {name}")
|
||||||
|
self.client.click("btn_project_new")
|
||||||
|
time.sleep(1)
|
||||||
|
self.client.set_value("project_git_dir", git_dir)
|
||||||
|
self.client.click("btn_project_save")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def create_discussion(self, name):
|
||||||
|
print(f"Creating discussion: {name}")
|
||||||
|
self.client.set_value("disc_new_name_input", name)
|
||||||
|
self.client.click("btn_disc_create")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def switch_discussion(self, name):
|
||||||
|
print(f"Switching to discussion: {name}")
|
||||||
|
self.client.select_list_item("disc_listbox", name)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def load_prior_log(self):
|
||||||
|
print("Loading prior log")
|
||||||
|
self.client.click("btn_load_log")
|
||||||
|
# This usually opens a file dialog which we can't easily automate from here
|
||||||
|
# without more hooks, but we can verify the button click.
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def truncate_history(self, pairs):
|
||||||
|
print(f"Truncating history to {pairs} pairs")
|
||||||
|
self.client.set_value("disc_truncate_pairs", pairs)
|
||||||
|
self.client.click("btn_disc_truncate")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def run_discussion_turn(self, user_message=None):
|
||||||
|
if user_message is None:
|
||||||
|
# Generate from AI history
|
||||||
|
session = self.client.get_session()
|
||||||
|
entries = session.get('session', {}).get('entries', [])
|
||||||
|
user_message = self.user_agent.generate_response(entries)
|
||||||
|
|
||||||
|
print(f"\n[USER]: {user_message}")
|
||||||
|
self.client.set_value("ai_input", user_message)
|
||||||
|
self.client.click("btn_gen_send")
|
||||||
|
|
||||||
|
# Wait for AI
|
||||||
|
return self.wait_for_ai_response()
|
||||||
|
|
||||||
|
def wait_for_ai_response(self, timeout=60):
|
||||||
|
print("Waiting for AI response...", end="", flush=True)
|
||||||
|
start_time = time.time()
|
||||||
|
last_count = len(self.client.get_session().get('session', {}).get('entries', []))
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
time.sleep(1)
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
entries = self.client.get_session().get('session', {}).get('entries', [])
|
||||||
|
if len(entries) > last_count:
|
||||||
|
last_entry = entries[-1]
|
||||||
|
if last_entry.get('role') == 'AI' and last_entry.get('content'):
|
||||||
|
print(f"\n[AI]: {last_entry.get('content')[:100]}...")
|
||||||
|
return last_entry
|
||||||
|
|
||||||
|
print("\nTimeout waiting for AI")
|
||||||
|
return None
|
||||||
+6
-2
@@ -32,11 +32,15 @@ def live_gui():
|
|||||||
"""
|
"""
|
||||||
print("\n[Fixture] Starting gui.py --enable-test-hooks...")
|
print("\n[Fixture] Starting gui.py --enable-test-hooks...")
|
||||||
|
|
||||||
|
# Ensure logs directory exists
|
||||||
|
os.makedirs("logs", exist_ok=True)
|
||||||
|
log_file = open("logs/gui_test.log", "w", encoding="utf-8")
|
||||||
|
|
||||||
# Start gui.py as a subprocess.
|
# Start gui.py as a subprocess.
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
["uv", "run", "python", "gui.py", "--enable-test-hooks"],
|
["uv", "run", "python", "gui.py", "--enable-test-hooks"],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=log_file,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=log_file,
|
||||||
text=True,
|
text=True,
|
||||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == 'nt' else 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
[project]
|
||||||
|
name = "temp_project"
|
||||||
|
git_dir = "C:\\projects\\manual_slop"
|
||||||
|
system_prompt = ""
|
||||||
|
main_context = ""
|
||||||
|
word_wrap = true
|
||||||
|
|
||||||
|
[output]
|
||||||
|
output_dir = "./md_gen"
|
||||||
|
|
||||||
|
[files]
|
||||||
|
base_dir = "."
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
[screenshots]
|
||||||
|
base_dir = "."
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
[agent.tools]
|
||||||
|
run_powershell = true
|
||||||
|
read_file = true
|
||||||
|
list_directory = true
|
||||||
|
search_files = true
|
||||||
|
get_file_summary = true
|
||||||
|
web_search = true
|
||||||
|
fetch_url = true
|
||||||
|
|
||||||
|
[discussion]
|
||||||
|
roles = [
|
||||||
|
"User",
|
||||||
|
"AI",
|
||||||
|
"Vendor API",
|
||||||
|
"System",
|
||||||
|
]
|
||||||
|
active = "main"
|
||||||
|
auto_add = true
|
||||||
|
|
||||||
|
[discussion.discussions.main]
|
||||||
|
git_commit = ""
|
||||||
|
last_updated = "2026-02-23T19:53:17"
|
||||||
|
history = []
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Ensure project root is in path for imports
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from api_hook_client import ApiHookClient
|
||||||
|
|
||||||
|
def test_api_client_has_extensions():
|
||||||
|
client = ApiHookClient()
|
||||||
|
# These should fail initially as they are not implemented
|
||||||
|
assert hasattr(client, 'select_tab')
|
||||||
|
assert hasattr(client, 'select_list_item')
|
||||||
|
|
||||||
|
def test_select_tab_integration(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
# We'll need to make sure the tags exist in gui.py
|
||||||
|
# For now, this is a placeholder for the integration test
|
||||||
|
response = client.select_tab("operations_tabs", "tab_tool")
|
||||||
|
assert response == {'status': 'queued'}
|
||||||
|
|
||||||
|
def test_select_list_item_integration(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
# Assuming 'Default' discussion exists or we can just test that it queues
|
||||||
|
response = client.select_list_item("disc_listbox", "Default")
|
||||||
|
assert response == {'status': 'queued'}
|
||||||
|
|
||||||
|
def test_get_indicator_state_integration(live_gui):
|
||||||
|
client = ApiHookClient()
|
||||||
|
# thinking_indicator is usually hidden unless AI is running
|
||||||
|
response = client.get_indicator_state("thinking_indicator")
|
||||||
|
assert 'shown' in response
|
||||||
|
assert response['tag'] == "thinking_indicator"
|
||||||
|
|
||||||
|
def test_app_processes_new_actions():
|
||||||
|
import gui
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
dpg.create_context()
|
||||||
|
try:
|
||||||
|
with patch('gui.load_config', return_value={}), \
|
||||||
|
patch('gui.PerformanceMonitor'), \
|
||||||
|
patch('gui.shell_runner'), \
|
||||||
|
patch('gui.project_manager'), \
|
||||||
|
patch.object(gui.App, '_load_active_project'):
|
||||||
|
app = gui.App()
|
||||||
|
|
||||||
|
with patch('dearpygui.dearpygui.set_value') as mock_set_value, \
|
||||||
|
patch('dearpygui.dearpygui.does_item_exist', return_value=True), \
|
||||||
|
patch('dearpygui.dearpygui.get_item_callback') as mock_get_cb:
|
||||||
|
|
||||||
|
# Test select_tab
|
||||||
|
app._pending_gui_tasks.append({
|
||||||
|
"action": "select_tab",
|
||||||
|
"tab_bar": "some_tab_bar",
|
||||||
|
"tab": "some_tab"
|
||||||
|
})
|
||||||
|
app._process_pending_gui_tasks()
|
||||||
|
mock_set_value.assert_any_call("some_tab_bar", "some_tab")
|
||||||
|
|
||||||
|
# Test select_list_item
|
||||||
|
mock_cb = MagicMock()
|
||||||
|
mock_get_cb.return_value = mock_cb
|
||||||
|
app._pending_gui_tasks.append({
|
||||||
|
"action": "select_list_item",
|
||||||
|
"listbox": "some_listbox",
|
||||||
|
"item_value": "some_value"
|
||||||
|
})
|
||||||
|
app._process_pending_gui_tasks()
|
||||||
|
mock_set_value.assert_any_call("some_listbox", "some_value")
|
||||||
|
mock_cb.assert_called_with("some_listbox", "some_value")
|
||||||
|
finally:
|
||||||
|
dpg.destroy_context()
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Ensure project root is in path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
# Load gui.py
|
||||||
|
spec = importlib.util.spec_from_file_location("gui", "gui.py")
|
||||||
|
gui = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["gui"] = gui
|
||||||
|
spec.loader.exec_module(gui)
|
||||||
|
from gui import App
|
||||||
|
|
||||||
|
def test_new_hubs_defined_in_window_info():
|
||||||
|
"""
|
||||||
|
Verifies that the new consolidated Hub windows are defined in the App's window_info.
|
||||||
|
This ensures they will be available in the 'Windows' menu.
|
||||||
|
"""
|
||||||
|
# We don't need a full App instance with DPG context for this,
|
||||||
|
# as window_info is initialized in __init__ before DPG starts.
|
||||||
|
# But we mock load_config to avoid file access.
|
||||||
|
from unittest.mock import patch
|
||||||
|
with patch('gui.load_config', return_value={}):
|
||||||
|
app = App()
|
||||||
|
|
||||||
|
expected_hubs = {
|
||||||
|
"Context Hub": "win_context_hub",
|
||||||
|
"AI Settings Hub": "win_ai_settings_hub",
|
||||||
|
"Discussion Hub": "win_discussion_hub",
|
||||||
|
"Operations Hub": "win_operations_hub",
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, tag in expected_hubs.items():
|
||||||
|
assert tag in app.window_info.values(), f"Expected window tag {tag} not found in window_info"
|
||||||
|
# Check if the label matches (or is present)
|
||||||
|
found = False
|
||||||
|
for l, t in app.window_info.items():
|
||||||
|
if t == tag:
|
||||||
|
found = True
|
||||||
|
assert l == label or label in l, f"Label mismatch for {tag}: expected {label}, found {l}"
|
||||||
|
assert found, f"Expected window label {label} not found in window_info"
|
||||||
|
|
||||||
|
def test_old_windows_removed_from_window_info(app_instance_simple):
|
||||||
|
"""
|
||||||
|
Verifies that the old fragmented windows are removed from window_info.
|
||||||
|
"""
|
||||||
|
old_tags = [
|
||||||
|
"win_projects", "win_files", "win_screenshots",
|
||||||
|
"win_provider", "win_system_prompts",
|
||||||
|
"win_discussion", "win_message", "win_response",
|
||||||
|
"win_comms", "win_tool_log"
|
||||||
|
]
|
||||||
|
|
||||||
|
for tag in old_tags:
|
||||||
|
assert tag not in app_instance_simple.window_info.values(), f"Old window tag {tag} should have been removed from window_info"
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance_simple():
|
||||||
|
from unittest.mock import patch
|
||||||
|
from gui import App
|
||||||
|
with patch('gui.load_config', return_value={}):
|
||||||
|
app = App()
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_hub_windows_have_correct_flags(app_instance_simple):
|
||||||
|
"""
|
||||||
|
Verifies that the new Hub windows have appropriate flags for a professional workspace.
|
||||||
|
(e.g., no_collapse should be True for main hubs).
|
||||||
|
"""
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
dpg.create_context()
|
||||||
|
|
||||||
|
# We need to actually call the build methods to check the configuration
|
||||||
|
app_instance_simple._build_context_hub()
|
||||||
|
app_instance_simple._build_ai_settings_hub()
|
||||||
|
app_instance_simple._build_discussion_hub()
|
||||||
|
app_instance_simple._build_operations_hub()
|
||||||
|
|
||||||
|
hubs = ["win_context_hub", "win_ai_settings_hub", "win_discussion_hub", "win_operations_hub"]
|
||||||
|
for hub in hubs:
|
||||||
|
assert dpg.does_item_exist(hub)
|
||||||
|
# We can't easily check 'no_collapse' after creation without internal DPG calls
|
||||||
|
# but we can check if it's been configured if we mock dpg.window or check it manually
|
||||||
|
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
def test_indicators_exist(app_instance_simple):
|
||||||
|
"""
|
||||||
|
Verifies that the new thinking and live indicators exist in the UI.
|
||||||
|
"""
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
dpg.create_context()
|
||||||
|
|
||||||
|
app_instance_simple._build_discussion_hub()
|
||||||
|
app_instance_simple._build_operations_hub()
|
||||||
|
|
||||||
|
assert dpg.does_item_exist("thinking_indicator")
|
||||||
|
assert dpg.does_item_exist("operations_live_indicator")
|
||||||
|
|
||||||
|
dpg.destroy_context()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Ensure project root is in path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from api_hook_client import ApiHookClient
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_full_live_workflow(live_gui):
|
||||||
|
"""
|
||||||
|
Integration test that drives the GUI through a full workflow.
|
||||||
|
"""
|
||||||
|
client = ApiHookClient()
|
||||||
|
assert client.wait_for_server(timeout=10)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 1. Reset
|
||||||
|
client.click("btn_reset")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 2. Project Setup
|
||||||
|
temp_project_path = os.path.abspath("tests/temp_project.toml")
|
||||||
|
if os.path.exists(temp_project_path):
|
||||||
|
os.remove(temp_project_path)
|
||||||
|
|
||||||
|
client.click("btn_project_new_automated", user_data=temp_project_path)
|
||||||
|
time.sleep(1) # Wait for project creation and switch
|
||||||
|
|
||||||
|
# Verify metadata update
|
||||||
|
proj = client.get_project()
|
||||||
|
|
||||||
|
test_git = os.path.abspath(".")
|
||||||
|
client.set_value("project_git_dir", test_git)
|
||||||
|
client.click("btn_project_save")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
proj = client.get_project()
|
||||||
|
# flat_config returns {"project": {...}, "output": ...}
|
||||||
|
# so proj is {"project": {"project": {"git_dir": ...}}}
|
||||||
|
assert proj['project']['project']['git_dir'] == test_git
|
||||||
|
|
||||||
|
# Enable auto-add so the response ends up in history
|
||||||
|
client.set_value("auto_add_history", True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 3. Discussion Turn
|
||||||
|
client.set_value("ai_input", "Hello! This is an automated test. Just say 'Acknowledged'.")
|
||||||
|
client.click("btn_gen_send")
|
||||||
|
|
||||||
|
# Verify thinking indicator appears (might be brief)
|
||||||
|
thinking_seen = False
|
||||||
|
print("\nPolling for thinking indicator...")
|
||||||
|
for i in range(20):
|
||||||
|
state = client.get_indicator_state("thinking_indicator")
|
||||||
|
if state.get('shown'):
|
||||||
|
thinking_seen = True
|
||||||
|
print(f"Thinking indicator seen at poll {i}")
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 4. Wait for response in session
|
||||||
|
success = False
|
||||||
|
print("Waiting for AI response in session...")
|
||||||
|
for i in range(60):
|
||||||
|
session = client.get_session()
|
||||||
|
entries = session.get('session', {}).get('entries', [])
|
||||||
|
if any(e.get('role') == 'AI' for e in entries):
|
||||||
|
success = True
|
||||||
|
print(f"AI response found at second {i}")
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
assert success, "AI failed to respond within 60 seconds"
|
||||||
|
|
||||||
|
# 5. Switch Discussion
|
||||||
|
client.set_value("disc_new_name_input", "AutoDisc")
|
||||||
|
client.click("btn_disc_create")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
client.select_list_item("disc_listbox", "AutoDisc")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Verify session is empty in new discussion
|
||||||
|
session = client.get_session()
|
||||||
|
assert len(session.get('session', {}).get('entries', [])) == 0
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Ensure project root is in path for imports
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from simulation.user_agent import UserSimAgent
|
||||||
|
|
||||||
|
def test_user_agent_instantiation():
|
||||||
|
agent = UserSimAgent(hook_client=None)
|
||||||
|
assert agent is not None
|
||||||
|
|
||||||
|
def test_perform_action_with_delay():
|
||||||
|
agent = UserSimAgent(hook_client=None)
|
||||||
|
called = False
|
||||||
|
def action():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
|
||||||
|
agent.perform_action_with_delay(action)
|
||||||
|
assert called is True
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# Ensure project root is in path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from simulation.workflow_sim import WorkflowSimulator
|
||||||
|
|
||||||
|
def test_simulator_instantiation():
|
||||||
|
client = MagicMock()
|
||||||
|
sim = WorkflowSimulator(client)
|
||||||
|
assert sim is not None
|
||||||
|
|
||||||
|
def test_setup_new_project():
|
||||||
|
client = MagicMock()
|
||||||
|
sim = WorkflowSimulator(client)
|
||||||
|
|
||||||
|
# Mock responses for wait_for_server
|
||||||
|
client.wait_for_server.return_value = True
|
||||||
|
|
||||||
|
sim.setup_new_project("TestProject", "/tmp/test_git")
|
||||||
|
|
||||||
|
# Verify hook calls
|
||||||
|
client.click.assert_any_call("btn_project_new")
|
||||||
|
client.set_value.assert_any_call("project_git_dir", "/tmp/test_git")
|
||||||
|
client.click.assert_any_call("btn_project_save")
|
||||||
|
|
||||||
|
def test_discussion_switching():
|
||||||
|
client = MagicMock()
|
||||||
|
sim = WorkflowSimulator(client)
|
||||||
|
|
||||||
|
sim.create_discussion("NewDisc")
|
||||||
|
client.set_value.assert_called_with("disc_new_name_input", "NewDisc")
|
||||||
|
client.click.assert_called_with("btn_disc_create")
|
||||||
|
|
||||||
|
sim.switch_discussion("NewDisc")
|
||||||
|
client.select_list_item.assert_called_with("disc_listbox", "NewDisc")
|
||||||
|
|
||||||
|
def test_history_truncation():
|
||||||
|
client = MagicMock()
|
||||||
|
sim = WorkflowSimulator(client)
|
||||||
|
|
||||||
|
sim.truncate_history(3)
|
||||||
|
client.set_value.assert_called_with("disc_truncate_pairs", 3)
|
||||||
|
client.click.assert_called_with("btn_disc_truncate")
|
||||||
+10
-4
@@ -5,7 +5,7 @@ Theming support for manual_slop GUI — imgui-bundle port.
|
|||||||
Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents.
|
Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents.
|
||||||
Palettes are applied via imgui.get_style().set_color_() calls.
|
Palettes are applied via imgui.get_style().set_color_() calls.
|
||||||
Font loading uses hello_imgui.load_font().
|
Font loading uses hello_imgui.load_font().
|
||||||
Scale uses imgui.get_io().font_global_scale.
|
Scale uses imgui.get_style().font_scale_main.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from imgui_bundle import imgui, hello_imgui
|
from imgui_bundle import imgui, hello_imgui
|
||||||
@@ -238,11 +238,11 @@ def apply(palette_name: str):
|
|||||||
|
|
||||||
|
|
||||||
def set_scale(factor: float):
|
def set_scale(factor: float):
|
||||||
"""Set the global font scale factor."""
|
"""Set the global font/UI scale factor."""
|
||||||
global _current_scale
|
global _current_scale
|
||||||
_current_scale = factor
|
_current_scale = factor
|
||||||
io = imgui.get_io()
|
style = imgui.get_style()
|
||||||
io.font_global_scale = factor
|
style.font_scale_main = factor
|
||||||
|
|
||||||
|
|
||||||
def save_to_config(config: dict):
|
def save_to_config(config: dict):
|
||||||
@@ -263,6 +263,12 @@ def load_from_config(config: dict):
|
|||||||
_current_font_size = float(t.get("font_size", 16.0))
|
_current_font_size = float(t.get("font_size", 16.0))
|
||||||
_current_scale = float(t.get("scale", 1.0))
|
_current_scale = float(t.get("scale", 1.0))
|
||||||
|
|
||||||
|
# Don't apply here — imgui context may not exist yet.
|
||||||
|
# Call apply_current() after imgui is initialised.
|
||||||
|
|
||||||
|
|
||||||
|
def apply_current():
|
||||||
|
"""Apply the loaded palette and scale. Call after imgui context exists."""
|
||||||
apply(_current_palette)
|
apply(_current_palette)
|
||||||
set_scale(_current_scale)
|
set_scale(_current_scale)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user