feat(types): Complete strict static analysis and typing track
This commit is contained in:
43
aggregate.py
43
aggregate.py
@@ -16,7 +16,7 @@ import tomllib
|
||||
import re
|
||||
import glob
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
import summarize
|
||||
import project_manager
|
||||
from file_cache import ASTParser
|
||||
@@ -67,9 +67,11 @@ def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> st
|
||||
sections = []
|
||||
for entry_raw in files:
|
||||
if isinstance(entry_raw, dict):
|
||||
entry = entry_raw.get("path")
|
||||
entry = cast(str, entry_raw.get("path", ""))
|
||||
else:
|
||||
entry = entry_raw
|
||||
if not entry or not isinstance(entry, str):
|
||||
continue
|
||||
paths = resolve_paths(base_dir, entry)
|
||||
if not paths:
|
||||
sections.append(f"### `{entry}`\n\n```text\nERROR: no files matched: {entry}\n```")
|
||||
@@ -90,6 +92,8 @@ def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> st
|
||||
def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str:
|
||||
sections = []
|
||||
for entry in screenshots:
|
||||
if not entry or not isinstance(entry, str):
|
||||
continue
|
||||
paths = resolve_paths(base_dir, entry)
|
||||
if not paths:
|
||||
sections.append(f"### `{entry}`\n\n_ERROR: no files matched: {entry}_")
|
||||
@@ -115,14 +119,16 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
|
||||
mtime : float (last modification time, for skip-if-unchanged optimization)
|
||||
tier : int | None (optional tier for context management)
|
||||
"""
|
||||
items = []
|
||||
items: list[dict[str, Any]] = []
|
||||
for entry_raw in files:
|
||||
if isinstance(entry_raw, dict):
|
||||
entry = entry_raw.get("path")
|
||||
entry = cast(str, entry_raw.get("path", ""))
|
||||
tier = entry_raw.get("tier")
|
||||
else:
|
||||
entry = entry_raw
|
||||
tier = None
|
||||
if not entry or not isinstance(entry, str):
|
||||
continue
|
||||
paths = resolve_paths(base_dir, entry)
|
||||
if not paths:
|
||||
items.append({"path": None, "entry": entry, "content": f"ERROR: no files matched: {entry}", "error": True, "mtime": 0.0, "tier": tier})
|
||||
@@ -156,14 +162,15 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
|
||||
sections = []
|
||||
for item in file_items:
|
||||
path = item.get("path")
|
||||
entry = item.get("entry", "unknown")
|
||||
content = item.get("content", "")
|
||||
entry = cast(str, item.get("entry", "unknown"))
|
||||
content = cast(str, item.get("content", ""))
|
||||
if path is None:
|
||||
sections.append(f"### `{entry}`\n\n```text\n{content}\n```")
|
||||
continue
|
||||
suffix = path.suffix.lstrip(".") if hasattr(path, "suffix") else "text"
|
||||
p = cast(Path, path)
|
||||
suffix = p.suffix.lstrip(".") if hasattr(p, "suffix") else "text"
|
||||
lang = suffix if suffix else "text"
|
||||
original = entry if "*" not in entry else str(path)
|
||||
original = entry if "*" not in entry else str(p)
|
||||
sections.append(f"### `{original}`\n\n```{lang}\n{content}\n```")
|
||||
return "\n\n---\n\n".join(sections)
|
||||
|
||||
@@ -205,15 +212,16 @@ def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
sections = []
|
||||
for item in file_items:
|
||||
path = item.get("path")
|
||||
name = path.name if path else ""
|
||||
name = path.name if path and isinstance(path, Path) else ""
|
||||
if name in core_files or item.get("tier") == 1:
|
||||
# Include in full
|
||||
sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" +
|
||||
f"```{path.suffix.lstrip('.') if path.suffix else 'text'}\n{item.get('content', '')}\n```")
|
||||
sections.append("### `" + (cast(str, item.get("entry")) or str(path)) + "`\n\n" +
|
||||
f"```{path.suffix.lstrip('.') if path and isinstance(path, Path) and path.suffix else 'text'}\n{item.get('content', '')}\n```")
|
||||
else:
|
||||
# Summarize
|
||||
sections.append("### `" + (item.get("entry") or str(path)) + "`\n\n" +
|
||||
summarize.summarise_file(path, item.get("content", "")))
|
||||
if path and isinstance(path, Path):
|
||||
sections.append("### `" + (cast(str, item.get("entry")) or str(path)) + "`\n\n" +
|
||||
summarize.summarise_file(path, cast(str, item.get("content", ""))))
|
||||
parts.append("## Files (Tier 1 - Mixed)\n\n" + "\n\n---\n\n".join(sections))
|
||||
if screenshots:
|
||||
parts.append("## Screenshots\n\n" + build_screenshots_section(screenshot_base_dir, screenshots))
|
||||
@@ -237,20 +245,20 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
if file_items:
|
||||
sections = []
|
||||
for item in file_items:
|
||||
path = item.get("path")
|
||||
entry = item.get("entry", "")
|
||||
path = cast(Path, item.get("path"))
|
||||
entry = cast(str, item.get("entry", ""))
|
||||
path_str = str(path) if path else ""
|
||||
# Check if this file is in focus_files (by name or path)
|
||||
is_focus = False
|
||||
for focus in focus_files:
|
||||
if focus == entry or (path and focus == path.name) or focus in path_str:
|
||||
if focus == entry or (path and focus == path.name) or (path_str and focus in path_str):
|
||||
is_focus = True
|
||||
break
|
||||
if is_focus or item.get("tier") == 3:
|
||||
sections.append("### `" + (entry or path_str) + "`\n\n" +
|
||||
f"```{path.suffix.lstrip('.') if path and path.suffix else 'text'}\n{item.get('content', '')}\n```")
|
||||
else:
|
||||
content = item.get("content", "")
|
||||
content = cast(str, item.get("content", ""))
|
||||
if path and path.suffix == ".py" and not item.get("error"):
|
||||
try:
|
||||
parser = ASTParser("python")
|
||||
@@ -260,6 +268,7 @@ def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
# Fallback to summary if AST parsing fails
|
||||
sections.append(f"### `{entry or path_str}`\n\n" + summarize.summarise_file(path, content))
|
||||
else:
|
||||
if path:
|
||||
sections.append(f"### `{entry or path_str}`\n\n" + summarize.summarise_file(path, content))
|
||||
parts.append("## Files (Tier 3 - Focused)\n\n" + "\n\n---\n\n".join(sections))
|
||||
if screenshots:
|
||||
|
||||
523
ai_client.py
523
ai_client.py
File diff suppressed because it is too large
Load Diff
@@ -22,20 +22,19 @@
|
||||
- [x] SAFETY: Preserve JSON serialization compatibility.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Core Library' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: GUI God-Object Typing Resolution
|
||||
- [ ] Task: Resolve `gui_2.py` Type Errors
|
||||
- [ ] WHERE: `gui_2.py`
|
||||
- [ ] WHAT: Type the `App` class state variables, method signatures, and ImGui integration boundaries.
|
||||
- [ ] HOW: Use `type: ignore[import]` only for ImGui C-bindings if strictly necessary, but type internal state tightly.
|
||||
- [ ] SAFETY: Ensure `live_gui` tests pass after typing.
|
||||
- [x] PROGRESS: Initial pass completed, several critical errors resolved, baseline established.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Typing' (Protocol in workflow.md)
|
||||
## Phase 3: GUI God-Object Typing Resolution [checkpoint: 6ebbf40]
|
||||
- [x] Task: Resolve `gui_2.py` Type Errors
|
||||
- [x] WHERE: `gui_2.py`
|
||||
- [x] WHAT: Type the `App` class state variables, method signatures, and ImGui integration boundaries.
|
||||
- [x] HOW: Use `type: ignore[import]` only for ImGui C-bindings if strictly necessary, but type internal state tightly.
|
||||
- [x] SAFETY: Ensure `live_gui` tests pass after typing.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Typing' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: CI Integration & Final Validation
|
||||
## Phase 4: CI Integration & Final Validation [checkpoint: c6c2a1b]
|
||||
- [x] Task: Establish Pre-Commit Guardrails
|
||||
- [x] WHERE: `.git/hooks/pre-commit` or a `scripts/validate_types.ps1`
|
||||
- [x] WHAT: Create a script that runs ruff and mypy, blocking commits if they fail.
|
||||
- [x] HOW: Standard shell scripting.
|
||||
- [x] SAFETY: Ensure it works cross-platform (Windows/Linux).
|
||||
- [ ] Task: Full Suite Validation & Warning Cleanup
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Validation' (Protocol in workflow.md)
|
||||
- [x] Task: Full Suite Validation & Warning Cleanup
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Validation' (Protocol in workflow.md)
|
||||
@@ -6,7 +6,7 @@ This file is kept so that any stale imports do not break.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Any, List, Tuple, Dict
|
||||
import tree_sitter
|
||||
import tree_sitter_python
|
||||
|
||||
@@ -33,15 +33,15 @@ class ASTParser:
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
tree = self.parse(code)
|
||||
edits = []
|
||||
edits: List[Tuple[int, int, str]] = []
|
||||
|
||||
def is_docstring(node):
|
||||
def is_docstring(node: tree_sitter.Node) -> bool:
|
||||
if node.type == "expression_statement" and node.child_count > 0:
|
||||
if node.children[0].type == "string":
|
||||
return True
|
||||
return False
|
||||
|
||||
def walk(node):
|
||||
def walk(node: tree_sitter.Node) -> None:
|
||||
if node.type == "function_definition":
|
||||
body = node.child_by_field_name("body")
|
||||
if body and body.type == "block":
|
||||
@@ -77,15 +77,15 @@ class ASTParser:
|
||||
Otherwise strips bodies but preserves docstrings.
|
||||
"""
|
||||
tree = self.parse(code)
|
||||
edits = []
|
||||
edits: List[Tuple[int, int, str]] = []
|
||||
|
||||
def is_docstring(node):
|
||||
def is_docstring(node: tree_sitter.Node) -> bool:
|
||||
if node.type == "expression_statement" and node.child_count > 0:
|
||||
if node.children[0].type == "string":
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_core_logic_decorator(node):
|
||||
def has_core_logic_decorator(node: tree_sitter.Node) -> bool:
|
||||
# Check if parent is decorated_definition
|
||||
parent = node.parent
|
||||
if parent and parent.type == "decorated_definition":
|
||||
@@ -96,7 +96,7 @@ class ASTParser:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_hot_comment(func_node):
|
||||
def has_hot_comment(func_node: tree_sitter.Node) -> bool:
|
||||
# Check all descendants of the function_definition for a [HOT] comment
|
||||
stack = [func_node]
|
||||
while stack:
|
||||
@@ -109,7 +109,7 @@ class ASTParser:
|
||||
stack.append(child)
|
||||
return False
|
||||
|
||||
def walk(node):
|
||||
def walk(node: tree_sitter.Node) -> None:
|
||||
if node.type == "function_definition":
|
||||
body = node.child_by_field_name("body")
|
||||
if body and body.type == "block":
|
||||
@@ -153,5 +153,6 @@ def get_file_id(path: Path) -> Optional[str]:
|
||||
def evict(path: Path) -> None:
|
||||
pass
|
||||
|
||||
def list_cached() -> list[dict]:
|
||||
def list_cached() -> List[Dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ class GeminiCliAdapter:
|
||||
def __init__(self, binary_path: str = "gemini"):
|
||||
self.binary_path = binary_path
|
||||
self.session_id: Optional[str] = None
|
||||
self.last_usage: Optional[dict] = None
|
||||
self.last_usage: Optional[dict[str, Any]] = None
|
||||
self.last_latency: float = 0.0
|
||||
|
||||
def send(self, message: str, safety_settings: list | None = None, system_instruction: str | None = None,
|
||||
def send(self, message: str, safety_settings: list[Any] | None = None, system_instruction: str | None = None,
|
||||
model: str | None = None, stream_callback: Optional[Callable[[str], None]] = None) -> dict[str, Any]:
|
||||
"""
|
||||
Sends a message to the Gemini CLI and processes the streaming JSON output.
|
||||
|
||||
346
gui_2.py
346
gui_2.py
@@ -9,7 +9,7 @@ import json
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
import requests
|
||||
import requests # type: ignore[import-untyped]
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Callable, Any
|
||||
@@ -61,21 +61,21 @@ def hide_tk_root() -> Tk:
|
||||
|
||||
def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: return imgui.ImVec4(r/255, g/255, b/255, a)
|
||||
|
||||
C_OUT: tuple[float, ...] = vec4(100, 200, 255)
|
||||
C_IN: tuple[float, ...] = vec4(140, 255, 160)
|
||||
C_REQ: tuple[float, ...] = vec4(255, 220, 100)
|
||||
C_RES: tuple[float, ...] = vec4(180, 255, 180)
|
||||
C_TC: tuple[float, ...] = vec4(255, 180, 80)
|
||||
C_TR: tuple[float, ...] = vec4(180, 220, 255)
|
||||
C_TRS: tuple[float, ...] = vec4(200, 180, 255)
|
||||
C_LBL: tuple[float, ...] = vec4(180, 180, 180)
|
||||
C_VAL: tuple[float, ...] = vec4(220, 220, 220)
|
||||
C_KEY: tuple[float, ...] = vec4(140, 200, 255)
|
||||
C_NUM: tuple[float, ...] = vec4(180, 255, 180)
|
||||
C_SUB: tuple[float, ...] = vec4(220, 200, 120)
|
||||
C_OUT: imgui.ImVec4 = vec4(100, 200, 255)
|
||||
C_IN: imgui.ImVec4 = vec4(140, 255, 160)
|
||||
C_REQ: imgui.ImVec4 = vec4(255, 220, 100)
|
||||
C_RES: imgui.ImVec4 = vec4(180, 255, 180)
|
||||
C_TC: imgui.ImVec4 = vec4(255, 180, 80)
|
||||
C_TR: imgui.ImVec4 = vec4(180, 220, 255)
|
||||
C_TRS: imgui.ImVec4 = vec4(200, 180, 255)
|
||||
C_LBL: imgui.ImVec4 = vec4(180, 180, 180)
|
||||
C_VAL: imgui.ImVec4 = vec4(220, 220, 220)
|
||||
C_KEY: imgui.ImVec4 = vec4(140, 200, 255)
|
||||
C_NUM: imgui.ImVec4 = vec4(180, 255, 180)
|
||||
C_SUB: imgui.ImVec4 = vec4(220, 200, 120)
|
||||
|
||||
DIR_COLORS: dict[str, tuple[float, ...]] = {"OUT": C_OUT, "IN": C_IN}
|
||||
KIND_COLORS: dict[str, tuple[float, ...]] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
|
||||
DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN}
|
||||
KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS}
|
||||
HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"}
|
||||
|
||||
DISC_ROLES: list[str] = ["User", "AI", "Vendor API", "System"]
|
||||
@@ -173,19 +173,19 @@ class App:
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Initialize locks first to avoid initialization order issues
|
||||
self._send_thread_lock = threading.Lock()
|
||||
self._disc_entries_lock = threading.Lock()
|
||||
self._pending_comms_lock = threading.Lock()
|
||||
self._pending_tool_calls_lock = threading.Lock()
|
||||
self._pending_history_adds_lock = threading.Lock()
|
||||
self._pending_gui_tasks_lock = threading.Lock()
|
||||
self._pending_dialog_lock = threading.Lock()
|
||||
self._api_event_queue_lock = threading.Lock()
|
||||
self._send_thread_lock: threading.Lock = threading.Lock()
|
||||
self._disc_entries_lock: threading.Lock = threading.Lock()
|
||||
self._pending_comms_lock: threading.Lock = threading.Lock()
|
||||
self._pending_tool_calls_lock: threading.Lock = threading.Lock()
|
||||
self._pending_history_adds_lock: threading.Lock = threading.Lock()
|
||||
self._pending_gui_tasks_lock: threading.Lock = threading.Lock()
|
||||
self._pending_dialog_lock: threading.Lock = threading.Lock()
|
||||
self._api_event_queue_lock: threading.Lock = threading.Lock()
|
||||
|
||||
self.config = load_config()
|
||||
self.event_queue = events.AsyncEventQueue()
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self.config: dict[str, Any] = load_config()
|
||||
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
|
||||
self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
|
||||
self._loop_thread: threading.Thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self._loop_thread.start()
|
||||
ai_cfg = self.config.get("ai", {})
|
||||
self._current_provider: str = ai_cfg.get("provider", "gemini")
|
||||
@@ -208,33 +208,33 @@ class App:
|
||||
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
||||
with self._disc_entries_lock:
|
||||
self.disc_entries: list[dict[str, Any]] = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen")
|
||||
self.ui_files_base_dir = self.project.get("files", {}).get("base_dir", ".")
|
||||
self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".")
|
||||
self.ui_output_dir: str = self.project.get("output", {}).get("output_dir", "./md_gen")
|
||||
self.ui_files_base_dir: str = self.project.get("files", {}).get("base_dir", ".")
|
||||
self.ui_shots_base_dir: str = self.project.get("screenshots", {}).get("base_dir", ".")
|
||||
proj_meta = self.project.get("project", {})
|
||||
self.ui_project_git_dir = proj_meta.get("git_dir", "")
|
||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||
self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
|
||||
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
||||
self.ui_word_wrap = proj_meta.get("word_wrap", True)
|
||||
self.ui_summary_only = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||
self.ui_ai_input = ""
|
||||
self.ui_disc_new_name_input = ""
|
||||
self.ui_disc_new_role_input = ""
|
||||
self.ui_epic_input = ""
|
||||
self.ui_project_git_dir: str = proj_meta.get("git_dir", "")
|
||||
self.ui_project_main_context: str = proj_meta.get("main_context", "")
|
||||
self.ui_project_system_prompt: str = proj_meta.get("system_prompt", "")
|
||||
self.ui_gemini_cli_path: str = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
||||
self.ui_word_wrap: bool = proj_meta.get("word_wrap", True)
|
||||
self.ui_summary_only: bool = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history: bool = disc_sec.get("auto_add", False)
|
||||
self.ui_global_system_prompt: str = self.config.get("ai", {}).get("system_prompt", "")
|
||||
self.ui_ai_input: str = ""
|
||||
self.ui_disc_new_name_input: str = ""
|
||||
self.ui_disc_new_role_input: str = ""
|
||||
self.ui_epic_input: str = ""
|
||||
self.proposed_tracks: list[dict[str, Any]] = []
|
||||
self._show_track_proposal_modal = False
|
||||
self.ui_new_track_name = ""
|
||||
self.ui_new_track_desc = ""
|
||||
self.ui_new_track_type = "feature"
|
||||
self.ui_conductor_setup_summary = ""
|
||||
self.ui_last_script_text = ""
|
||||
self.ui_last_script_output = ""
|
||||
self.ai_status = "idle"
|
||||
self.ai_response = ""
|
||||
self.last_md = ""
|
||||
self._show_track_proposal_modal: bool = False
|
||||
self.ui_new_track_name: str = ""
|
||||
self.ui_new_track_desc: str = ""
|
||||
self.ui_new_track_type: str = "feature"
|
||||
self.ui_conductor_setup_summary: str = ""
|
||||
self.ui_last_script_text: str = ""
|
||||
self.ui_last_script_output: str = ""
|
||||
self.ai_status: str = "idle"
|
||||
self.ai_response: str = ""
|
||||
self.last_md: str = ""
|
||||
self.last_md_path: Path | None = None
|
||||
self.last_file_items: list[Any] = []
|
||||
self.send_thread: threading.Thread | None = None
|
||||
@@ -255,82 +255,82 @@ class App:
|
||||
"Diagnostics": False,
|
||||
}
|
||||
saved = self.config.get("gui", {}).get("show_windows", {})
|
||||
self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
|
||||
self.show_script_output = False
|
||||
self.show_text_viewer = False
|
||||
self.text_viewer_title = ""
|
||||
self.text_viewer_content = ""
|
||||
self.show_windows: dict[str, bool] = {k: saved.get(k, v) for k, v in _default_windows.items()}
|
||||
self.show_script_output: bool = False
|
||||
self.show_text_viewer: bool = False
|
||||
self.text_viewer_title: str = ""
|
||||
self.text_viewer_content: str = ""
|
||||
self._pending_dialog: ConfirmDialog | None = None
|
||||
self._pending_dialog_open = False
|
||||
self._pending_dialog_open: bool = False
|
||||
self._pending_actions: dict[str, ConfirmDialog] = {}
|
||||
self._pending_ask_dialog = False
|
||||
self._ask_dialog_open = False
|
||||
self._ask_request_id = None
|
||||
self._ask_tool_data = None
|
||||
self.mma_step_mode = False
|
||||
self._pending_ask_dialog: bool = False
|
||||
self._ask_dialog_open: bool = False
|
||||
self._ask_request_id: str | None = None
|
||||
self._ask_tool_data: dict[str, Any] | None = None
|
||||
self.mma_step_mode: bool = False
|
||||
self.active_track: Track | None = None
|
||||
self.active_tickets: list[dict[str, Any]] = []
|
||||
self.active_tier: str | None = None
|
||||
self.ui_focus_agent: str | None = None
|
||||
self.mma_status = "idle"
|
||||
self.mma_status: str = "idle"
|
||||
self._pending_mma_approval: dict[str, Any] | None = None
|
||||
self._mma_approval_open = False
|
||||
self._mma_approval_edit_mode = False
|
||||
self._mma_approval_payload = ""
|
||||
self._mma_approval_open: bool = False
|
||||
self._mma_approval_edit_mode: bool = False
|
||||
self._mma_approval_payload: str = ""
|
||||
self._pending_mma_spawn: dict[str, Any] | None = None
|
||||
self._mma_spawn_open = False
|
||||
self._mma_spawn_edit_mode = False
|
||||
self._mma_spawn_prompt = ''
|
||||
self._mma_spawn_context = ''
|
||||
self.mma_tier_usage = {
|
||||
self._mma_spawn_open: bool = False
|
||||
self._mma_spawn_edit_mode: bool = False
|
||||
self._mma_spawn_prompt: str = ''
|
||||
self._mma_spawn_context: str = ''
|
||||
self.mma_tier_usage: dict[str, dict[str, Any]] = {
|
||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
|
||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
}
|
||||
self._tool_log: list[dict] = []
|
||||
self._tool_log: list[dict[str, Any]] = []
|
||||
self._comms_log: list[dict[str, Any]] = []
|
||||
self._pending_comms: list[dict[str, Any]] = []
|
||||
self._pending_tool_calls: list[dict] = []
|
||||
self._pending_tool_calls: list[dict[str, Any]] = []
|
||||
self._pending_history_adds: list[dict[str, Any]] = []
|
||||
self._trigger_blink = False
|
||||
self._is_blinking = False
|
||||
self._blink_start_time = 0.0
|
||||
self._trigger_script_blink = False
|
||||
self._is_script_blinking = False
|
||||
self._script_blink_start_time = 0.0
|
||||
self._scroll_disc_to_bottom = False
|
||||
self._scroll_comms_to_bottom = False
|
||||
self._scroll_tool_calls_to_bottom = False
|
||||
self._trigger_blink: bool = False
|
||||
self._is_blinking: bool = False
|
||||
self._blink_start_time: float = 0.0
|
||||
self._trigger_script_blink: bool = False
|
||||
self._is_script_blinking: bool = False
|
||||
self._script_blink_start_time: float = 0.0
|
||||
self._scroll_disc_to_bottom: bool = False
|
||||
self._scroll_comms_to_bottom: bool = False
|
||||
self._scroll_tool_calls_to_bottom: bool = False
|
||||
self._pending_gui_tasks: list[dict[str, Any]] = []
|
||||
self.session_usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}
|
||||
self._gemini_cache_text = ""
|
||||
self.session_usage: dict[str, Any] = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "last_latency": 0.0}
|
||||
self._gemini_cache_text: str = ""
|
||||
self._last_stable_md: str = ''
|
||||
self._token_stats: dict = {}
|
||||
self._token_stats: dict[str, Any] = {}
|
||||
self._token_stats_dirty: bool = False
|
||||
self.ui_disc_truncate_pairs: int = 2
|
||||
self.ui_auto_scroll_comms = True
|
||||
self.ui_auto_scroll_tool_calls = True
|
||||
self.ui_auto_scroll_comms: bool = True
|
||||
self.ui_auto_scroll_tool_calls: bool = True
|
||||
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools: dict[str, bool] = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
self.tracks: list[dict[str, Any]] = []
|
||||
self._show_add_ticket_form = False
|
||||
self.ui_new_ticket_id = ""
|
||||
self.ui_new_ticket_desc = ""
|
||||
self.ui_new_ticket_target = ""
|
||||
self.ui_new_ticket_deps = ""
|
||||
self._track_discussion_active = False
|
||||
self._show_add_ticket_form: bool = False
|
||||
self.ui_new_ticket_id: str = ""
|
||||
self.ui_new_ticket_desc: str = ""
|
||||
self.ui_new_ticket_target: str = ""
|
||||
self.ui_new_ticket_deps: str = ""
|
||||
self._track_discussion_active: bool = False
|
||||
self.mma_streams: dict[str, str] = {}
|
||||
self._tier_stream_last_len: dict[str, int] = {}
|
||||
self.is_viewing_prior_session = False
|
||||
self.is_viewing_prior_session: bool = False
|
||||
self.prior_session_entries: list[dict[str, Any]] = []
|
||||
self.test_hooks_enabled = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||
self.ui_manual_approve = False
|
||||
self.perf_monitor = PerformanceMonitor()
|
||||
self.perf_history = {"frame_time": [0.0]*100, "fps": [0.0]*100, "cpu": [0.0]*100, "input_lag": [0.0]*100}
|
||||
self._perf_last_update = 0.0
|
||||
self._autosave_interval = 60.0
|
||||
self._last_autosave = time.time()
|
||||
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||
self.ui_manual_approve: bool = False
|
||||
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
|
||||
self.perf_history: dict[str, list[float]] = {"frame_time": [0.0]*100, "fps": [0.0]*100, "cpu": [0.0]*100, "input_lag": [0.0]*100}
|
||||
self._perf_last_update: float = 0.0
|
||||
self._autosave_interval: float = 60.0
|
||||
self._last_autosave: float = time.time()
|
||||
label = self.project.get("project", {}).get("name", "")
|
||||
session_logger.open_session(label=label)
|
||||
self._prune_old_logs()
|
||||
@@ -856,7 +856,7 @@ class App:
|
||||
self._switch_discussion(remaining[0])
|
||||
# ---------------------------------------------------------------- logic
|
||||
|
||||
def _on_comms_entry(self, entry: dict) -> None:
|
||||
def _on_comms_entry(self, entry: dict[str, Any]) -> None:
|
||||
# sys.stderr.write(f"[DEBUG] _on_comms_entry: {entry.get('kind')} {entry.get('direction')}\n")
|
||||
session_logger.log_comms(entry)
|
||||
entry["local_ts"] = time.time()
|
||||
@@ -898,7 +898,7 @@ class App:
|
||||
with self._pending_tool_calls_lock:
|
||||
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
|
||||
|
||||
def _on_api_event(self, *args, **kwargs) -> None:
|
||||
def _on_api_event(self, *args: Any, **kwargs: Any) -> None:
|
||||
payload = kwargs.get("payload", {})
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({"action": "refresh_api_metrics", "payload": payload})
|
||||
@@ -992,16 +992,16 @@ class App:
|
||||
setattr(self, attr_name, value)
|
||||
if item == "gcli_path":
|
||||
if not ai_client._gemini_cli_adapter:
|
||||
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=value)
|
||||
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(value))
|
||||
else:
|
||||
ai_client._gemini_cli_adapter.binary_path = value
|
||||
ai_client._gemini_cli_adapter.binary_path = str(value)
|
||||
elif action == "click":
|
||||
item = task.get("item")
|
||||
user_data = task.get("user_data")
|
||||
if item == "btn_project_new_automated":
|
||||
self._cb_new_project_automated(user_data)
|
||||
elif item == "btn_mma_load_track":
|
||||
self._cb_load_track(user_data)
|
||||
self._cb_load_track(str(user_data or ""))
|
||||
elif item in self._clickable_actions:
|
||||
# Check if it's a method that accepts user_data
|
||||
import inspect
|
||||
@@ -1018,7 +1018,7 @@ class App:
|
||||
item = task.get("listbox", task.get("item"))
|
||||
value = task.get("item_value", task.get("value"))
|
||||
if item == "disc_listbox":
|
||||
self._switch_discussion(value)
|
||||
self._switch_discussion(str(value or ""))
|
||||
elif task.get("type") == "ask":
|
||||
self._pending_ask_dialog = True
|
||||
self._ask_request_id = task.get("request_id")
|
||||
@@ -1037,18 +1037,18 @@ class App:
|
||||
elif cb in self._predefined_callbacks:
|
||||
self._predefined_callbacks[cb](*args)
|
||||
elif action == "mma_step_approval":
|
||||
dlg = MMAApprovalDialog(task.get("ticket_id"), task.get("payload"))
|
||||
dlg = MMAApprovalDialog(str(task.get("ticket_id") or ""), str(task.get("payload") or ""))
|
||||
self._pending_mma_approval = task
|
||||
if "dialog_container" in task:
|
||||
task["dialog_container"][0] = dlg
|
||||
elif action == 'refresh_from_project':
|
||||
self._refresh_from_project()
|
||||
elif action == "mma_spawn_approval":
|
||||
dlg = MMASpawnApprovalDialog(
|
||||
task.get("ticket_id"),
|
||||
task.get("role"),
|
||||
task.get("prompt"),
|
||||
task.get("context_md")
|
||||
spawn_dlg = MMASpawnApprovalDialog(
|
||||
str(task.get("ticket_id") or ""),
|
||||
str(task.get("role") or ""),
|
||||
str(task.get("prompt") or ""),
|
||||
str(task.get("context_md") or "")
|
||||
)
|
||||
self._pending_mma_spawn = task
|
||||
self._mma_spawn_prompt = task.get("prompt", "")
|
||||
@@ -1056,7 +1056,7 @@ class App:
|
||||
self._mma_spawn_open = True
|
||||
self._mma_spawn_edit_mode = False
|
||||
if "dialog_container" in task:
|
||||
task["dialog_container"][0] = dlg
|
||||
task["dialog_container"][0] = spawn_dlg
|
||||
except Exception as e:
|
||||
print(f"Error executing GUI task: {e}")
|
||||
|
||||
@@ -1143,7 +1143,7 @@ class App:
|
||||
else:
|
||||
print("[DEBUG] No pending spawn approval found")
|
||||
|
||||
def _handle_mma_respond(self, approved: bool, payload: str = None, abort: bool = False, prompt: str = None, context_md: str = None) -> None:
|
||||
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
|
||||
if self._pending_mma_approval:
|
||||
dlg = self._pending_mma_approval.get("dialog_container", [None])[0]
|
||||
if dlg:
|
||||
@@ -1155,17 +1155,17 @@ class App:
|
||||
dlg._condition.notify_all()
|
||||
self._pending_mma_approval = None
|
||||
if self._pending_mma_spawn:
|
||||
dlg = self._pending_mma_spawn.get("dialog_container", [None])[0]
|
||||
if dlg:
|
||||
with dlg._condition:
|
||||
dlg._approved = approved
|
||||
dlg._abort = abort
|
||||
spawn_dlg = self._pending_mma_spawn.get("dialog_container", [None])[0]
|
||||
if spawn_dlg:
|
||||
with spawn_dlg._condition:
|
||||
spawn_dlg._approved = approved
|
||||
spawn_dlg._abort = abort
|
||||
if prompt is not None:
|
||||
dlg._prompt = prompt
|
||||
spawn_dlg._prompt = prompt
|
||||
if context_md is not None:
|
||||
dlg._context_md = context_md
|
||||
dlg._done = True
|
||||
dlg._condition.notify_all()
|
||||
spawn_dlg._context_md = context_md
|
||||
spawn_dlg._done = True
|
||||
spawn_dlg._condition.notify_all()
|
||||
self._pending_mma_spawn = None
|
||||
|
||||
def _handle_approve_ask(self) -> None:
|
||||
@@ -1173,7 +1173,7 @@ class App:
|
||||
if not self._ask_request_id: return
|
||||
request_id = self._ask_request_id
|
||||
|
||||
def do_post():
|
||||
def do_post() -> None:
|
||||
try:
|
||||
requests.post(
|
||||
"http://127.0.0.1:8999/api/ask/respond",
|
||||
@@ -1191,7 +1191,7 @@ class App:
|
||||
if not self._ask_request_id: return
|
||||
request_id = self._ask_request_id
|
||||
|
||||
def do_post():
|
||||
def do_post() -> None:
|
||||
try:
|
||||
requests.post(
|
||||
"http://127.0.0.1:8999/api/ask/respond",
|
||||
@@ -1268,7 +1268,7 @@ class App:
|
||||
self._loop.create_task(self._process_event_queue())
|
||||
|
||||
# Fallback: process queues even if GUI thread is idling/stuck
|
||||
async def queue_fallback():
|
||||
async def queue_fallback() -> None:
|
||||
while True:
|
||||
try:
|
||||
self._process_pending_gui_tasks()
|
||||
@@ -1393,7 +1393,7 @@ class App:
|
||||
usage[k] += u.get(k, 0) or 0
|
||||
self.session_usage = usage
|
||||
|
||||
def _refresh_api_metrics(self, payload: dict, md_content: str | None = None) -> None:
|
||||
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
|
||||
if "latency" in payload:
|
||||
self.session_usage["last_latency"] = payload["latency"]
|
||||
self._recalculate_session_usage()
|
||||
@@ -1567,7 +1567,7 @@ class App:
|
||||
self.config["gui"] = {"show_windows": self.show_windows}
|
||||
theme.save_to_config(self.config)
|
||||
|
||||
def _do_generate(self) -> tuple[str, Path, list, str, str]:
|
||||
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
|
||||
"""Returns (full_md, output_path, file_items, stable_md, discussion_text)."""
|
||||
self._flush_to_project()
|
||||
self._save_active_project()
|
||||
@@ -1589,7 +1589,7 @@ class App:
|
||||
def _fetch_models(self, provider: str) -> None:
|
||||
self.ai_status = "fetching models..."
|
||||
|
||||
def do_fetch():
|
||||
def do_fetch() -> None:
|
||||
try:
|
||||
models = ai_client.list_models(provider)
|
||||
self.available_models = models
|
||||
@@ -1698,12 +1698,14 @@ class App:
|
||||
self._tool_log.append(tc)
|
||||
self._pending_tool_calls.clear()
|
||||
if self.show_windows.get("Context Hub", False):
|
||||
exp, self.show_windows["Context Hub"] = imgui.begin("Context Hub", self.show_windows["Context Hub"])
|
||||
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
|
||||
self.show_windows["Context Hub"] = bool(opened)
|
||||
if exp:
|
||||
self._render_projects_panel()
|
||||
imgui.end()
|
||||
if self.show_windows.get("Files & Media", False):
|
||||
exp, self.show_windows["Files & Media"] = imgui.begin("Files & Media", self.show_windows["Files & Media"])
|
||||
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"])
|
||||
self.show_windows["Files & Media"] = bool(opened)
|
||||
if exp:
|
||||
if imgui.collapsing_header("Files"):
|
||||
self._render_files_panel()
|
||||
@@ -1711,7 +1713,8 @@ class App:
|
||||
self._render_screenshots_panel()
|
||||
imgui.end()
|
||||
if self.show_windows.get("AI Settings", False):
|
||||
exp, self.show_windows["AI Settings"] = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"])
|
||||
self.show_windows["AI Settings"] = bool(opened)
|
||||
if exp:
|
||||
if imgui.collapsing_header("Provider & Model"):
|
||||
self._render_provider_panel()
|
||||
@@ -1721,34 +1724,40 @@ class App:
|
||||
self._render_token_budget_panel()
|
||||
imgui.end()
|
||||
if self.show_windows.get("MMA Dashboard", False):
|
||||
exp, self.show_windows["MMA Dashboard"] = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"])
|
||||
exp, opened = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"])
|
||||
self.show_windows["MMA Dashboard"] = bool(opened)
|
||||
if exp:
|
||||
self._render_mma_dashboard()
|
||||
imgui.end()
|
||||
if self.show_windows.get("Tier 1: Strategy", False):
|
||||
exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
|
||||
exp, opened = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
|
||||
self.show_windows["Tier 1: Strategy"] = bool(opened)
|
||||
if exp:
|
||||
self._render_tier_stream_panel("Tier 1", "Tier 1")
|
||||
imgui.end()
|
||||
if self.show_windows.get("Tier 2: Tech Lead", False):
|
||||
exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
|
||||
exp, opened = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
|
||||
self.show_windows["Tier 2: Tech Lead"] = bool(opened)
|
||||
if exp:
|
||||
self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)")
|
||||
imgui.end()
|
||||
if self.show_windows.get("Tier 3: Workers", False):
|
||||
exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
|
||||
exp, opened = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
|
||||
self.show_windows["Tier 3: Workers"] = bool(opened)
|
||||
if exp:
|
||||
self._render_tier_stream_panel("Tier 3", None)
|
||||
imgui.end()
|
||||
if self.show_windows.get("Tier 4: QA", False):
|
||||
exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
|
||||
exp, opened = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
|
||||
self.show_windows["Tier 4: QA"] = bool(opened)
|
||||
if exp:
|
||||
self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)")
|
||||
imgui.end()
|
||||
if self.show_windows.get("Theme", False):
|
||||
self._render_theme_panel()
|
||||
if self.show_windows.get("Discussion Hub", False):
|
||||
exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
|
||||
exp, opened = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
|
||||
self.show_windows["Discussion Hub"] = bool(opened)
|
||||
if exp:
|
||||
# Top part for the history
|
||||
imgui.begin_child("HistoryChild", size=(0, -200))
|
||||
@@ -1765,7 +1774,8 @@ class App:
|
||||
imgui.end_tab_bar()
|
||||
imgui.end()
|
||||
if self.show_windows.get("Operations Hub", False):
|
||||
exp, self.show_windows["Operations Hub"] = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
|
||||
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
|
||||
self.show_windows["Operations Hub"] = bool(opened)
|
||||
if exp:
|
||||
imgui.text("Focus Agent:")
|
||||
imgui.same_line()
|
||||
@@ -1794,7 +1804,8 @@ class App:
|
||||
if self.show_windows.get("Log Management", False):
|
||||
self._render_log_management()
|
||||
if self.show_windows["Diagnostics"]:
|
||||
exp, self.show_windows["Diagnostics"] = imgui.begin("Diagnostics", self.show_windows["Diagnostics"])
|
||||
exp, opened = imgui.begin("Diagnostics", self.show_windows["Diagnostics"])
|
||||
self.show_windows["Diagnostics"] = bool(opened)
|
||||
if exp:
|
||||
now = time.time()
|
||||
if now - self._perf_last_update >= 0.5:
|
||||
@@ -1893,7 +1904,7 @@ class App:
|
||||
else:
|
||||
self._ask_dialog_open = False
|
||||
if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
if not self._pending_ask_dialog:
|
||||
if not self._pending_ask_dialog or self._ask_tool_data is None:
|
||||
imgui.close_current_popup()
|
||||
else:
|
||||
tool_name = self._ask_tool_data.get("tool", "unknown")
|
||||
@@ -2000,7 +2011,7 @@ class App:
|
||||
self._is_script_blinking = True
|
||||
self._script_blink_start_time = time.time()
|
||||
try:
|
||||
imgui.set_window_focus("Last Script Output")
|
||||
imgui.set_window_focus("Last Script Output") # type: ignore[call-arg]
|
||||
except Exception:
|
||||
pass
|
||||
if self._is_script_blinking:
|
||||
@@ -2013,7 +2024,8 @@ class App:
|
||||
imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 100, 255, alpha))
|
||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(0, 100, 255, alpha))
|
||||
imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever)
|
||||
expanded, self.show_script_output = imgui.begin("Last Script Output", self.show_script_output)
|
||||
expanded, opened = imgui.begin("Last Script Output", self.show_script_output)
|
||||
self.show_script_output = bool(opened)
|
||||
if expanded:
|
||||
imgui.text("Script:")
|
||||
imgui.same_line()
|
||||
@@ -2043,7 +2055,8 @@ class App:
|
||||
imgui.end()
|
||||
if self.show_text_viewer:
|
||||
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
|
||||
expanded, self.show_text_viewer = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
|
||||
expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer)
|
||||
self.show_text_viewer = bool(opened)
|
||||
if expanded:
|
||||
if self.ui_word_wrap:
|
||||
imgui.begin_child("tv_wrap", imgui.ImVec2(-1, -1), False)
|
||||
@@ -2153,7 +2166,7 @@ class App:
|
||||
self._cb_plan_epic()
|
||||
|
||||
def _cb_plan_epic(self) -> None:
|
||||
def _bg_task():
|
||||
def _bg_task() -> None:
|
||||
try:
|
||||
self.ai_status = "Planning Epic (Tier 1)..."
|
||||
history = orchestrator_pm.get_track_history_summary()
|
||||
@@ -2166,7 +2179,7 @@ class App:
|
||||
_t1_resp = [e for e in _t1_new if e.get("direction") == "IN" and e.get("kind") == "response"]
|
||||
_t1_in = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _t1_resp)
|
||||
_t1_out = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _t1_resp)
|
||||
def _push_t1_usage(i, o):
|
||||
def _push_t1_usage(i: int, o: int) -> None:
|
||||
self.mma_tier_usage["Tier 1"]["input"] += i
|
||||
self.mma_tier_usage["Tier 1"]["output"] += o
|
||||
with self._pending_gui_tasks_lock:
|
||||
@@ -2194,7 +2207,7 @@ class App:
|
||||
|
||||
def _cb_accept_tracks(self) -> None:
|
||||
self._show_track_proposal_modal = False
|
||||
def _bg_task():
|
||||
def _bg_task() -> None:
|
||||
# Generate skeletons once
|
||||
self.ai_status = "Phase 2: Generating skeletons for all tracks..."
|
||||
parser = ASTParser(language="python")
|
||||
@@ -2374,7 +2387,8 @@ class App:
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_log_management(self) -> None:
|
||||
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
|
||||
exp, opened = imgui.begin("Log Management", self.show_windows["Log Management"])
|
||||
self.show_windows["Log Management"] = bool(opened)
|
||||
if not exp:
|
||||
imgui.end()
|
||||
return
|
||||
@@ -2413,19 +2427,19 @@ class App:
|
||||
if imgui.button(f"Unstar##{session_id}"):
|
||||
registry.update_session_metadata(
|
||||
session_id,
|
||||
message_count=metadata.get("message_count"),
|
||||
errors=metadata.get("errors"),
|
||||
size_kb=metadata.get("size_kb"),
|
||||
message_count=int(metadata.get("message_count") or 0),
|
||||
errors=int(metadata.get("errors") or 0),
|
||||
size_kb=int(metadata.get("size_kb") or 0),
|
||||
whitelisted=False,
|
||||
reason=metadata.get("reason")
|
||||
reason=str(metadata.get("reason") or "")
|
||||
)
|
||||
else:
|
||||
if imgui.button(f"Star##{session_id}"):
|
||||
registry.update_session_metadata(
|
||||
session_id,
|
||||
message_count=metadata.get("message_count"),
|
||||
errors=metadata.get("errors"),
|
||||
size_kb=metadata.get("size_kb"),
|
||||
message_count=int(metadata.get("message_count") or 0),
|
||||
errors=int(metadata.get("errors") or 0),
|
||||
size_kb=int(metadata.get("size_kb") or 0),
|
||||
whitelisted=True,
|
||||
reason="Manually whitelisted"
|
||||
)
|
||||
@@ -2867,7 +2881,7 @@ class App:
|
||||
self._is_blinking = True
|
||||
self._blink_start_time = time.time()
|
||||
try:
|
||||
imgui.set_window_focus("Response")
|
||||
imgui.set_window_focus("Response") # type: ignore[call-arg]
|
||||
except:
|
||||
pass
|
||||
is_blinking = False
|
||||
@@ -3131,7 +3145,7 @@ class App:
|
||||
imgui.pop_style_color()
|
||||
imgui.table_next_column()
|
||||
if imgui.button(f"Load##{track.get('id')}"):
|
||||
self._cb_load_track(track.get("id"))
|
||||
self._cb_load_track(str(track.get("id") or ""))
|
||||
imgui.end_table()
|
||||
|
||||
# 1b. New Track Form
|
||||
@@ -3248,14 +3262,14 @@ class App:
|
||||
# 4. Task DAG Visualizer
|
||||
imgui.text("Task DAG")
|
||||
if self.active_track:
|
||||
tickets_by_id = {t.get('id'): t for t in self.active_tickets}
|
||||
tickets_by_id = {str(t.get('id') or ''): t for t in self.active_tickets}
|
||||
all_ids = set(tickets_by_id.keys())
|
||||
# Build children map
|
||||
children_map = {}
|
||||
children_map: dict[str, list[str]] = {}
|
||||
for t in self.active_tickets:
|
||||
for dep in t.get('depends_on', []):
|
||||
if dep not in children_map: children_map[dep] = []
|
||||
children_map[dep].append(t.get('id'))
|
||||
children_map[dep].append(str(t.get('id') or ''))
|
||||
# Roots are those whose depends_on elements are NOT in all_ids
|
||||
roots = []
|
||||
for t in self.active_tickets:
|
||||
@@ -3263,7 +3277,7 @@ class App:
|
||||
has_local_dep = any(d in all_ids for d in deps)
|
||||
if not has_local_dep:
|
||||
roots.append(t)
|
||||
rendered = set()
|
||||
rendered: set[str] = set()
|
||||
for root in roots:
|
||||
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
|
||||
|
||||
@@ -3341,7 +3355,7 @@ class App:
|
||||
pass
|
||||
imgui.end_child()
|
||||
|
||||
def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:
|
||||
def _render_ticket_dag_node(self, ticket: dict[str, Any], tickets_by_id: dict[str, Any], children_map: dict[str, list[str]], rendered: set[str]) -> None:
|
||||
tid = ticket.get('id', '??')
|
||||
is_duplicate = tid in rendered
|
||||
if not is_duplicate:
|
||||
@@ -3553,7 +3567,9 @@ class App:
|
||||
ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100))
|
||||
|
||||
def _render_theme_panel(self) -> None:
|
||||
exp, self.show_windows["Theme"] = imgui.begin("Theme", self.show_windows["Theme"])
|
||||
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
||||
self.show_windows["Theme"] = bool(opened)
|
||||
|
||||
if exp:
|
||||
imgui.text("Palette")
|
||||
cp = theme.get_current_palette()
|
||||
|
||||
BIN
gui_2_mypy.txt
Normal file
BIN
gui_2_mypy.txt
Normal file
Binary file not shown.
BIN
gui_2_only_errors.txt
Normal file
BIN
gui_2_only_errors.txt
Normal file
Binary file not shown.
@@ -3,6 +3,7 @@ import tomli_w
|
||||
import tomllib
|
||||
from datetime import datetime
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
class LogRegistry:
|
||||
"""
|
||||
@@ -18,7 +19,7 @@ class LogRegistry:
|
||||
registry_path (str): The file path to the TOML registry.
|
||||
"""
|
||||
self.registry_path = registry_path
|
||||
self.data = {}
|
||||
self.data: dict[str, dict[str, Any]] = {}
|
||||
self.load_registry()
|
||||
|
||||
def load_registry(self) -> None:
|
||||
@@ -56,16 +57,16 @@ class LogRegistry:
|
||||
"""
|
||||
try:
|
||||
# Convert datetime objects to ISO format strings for TOML serialization
|
||||
data_to_save = {}
|
||||
data_to_save: dict[str, Any] = {}
|
||||
for session_id, session_data in self.data.items():
|
||||
session_data_copy = {}
|
||||
session_data_copy: dict[str, Any] = {}
|
||||
for k, v in session_data.items():
|
||||
if v is None:
|
||||
continue
|
||||
if k == 'start_time' and isinstance(v, datetime):
|
||||
session_data_copy[k] = v.isoformat()
|
||||
elif k == 'metadata' and isinstance(v, dict):
|
||||
metadata_copy = {}
|
||||
metadata_copy: dict[str, Any] = {}
|
||||
for mk, mv in v.items():
|
||||
if mv is None:
|
||||
continue
|
||||
@@ -125,11 +126,13 @@ class LogRegistry:
|
||||
if self.data[session_id].get('metadata') is None:
|
||||
self.data[session_id]['metadata'] = {}
|
||||
# Update fields
|
||||
self.data[session_id]['metadata']['message_count'] = message_count
|
||||
self.data[session_id]['metadata']['errors'] = errors
|
||||
self.data[session_id]['metadata']['size_kb'] = size_kb
|
||||
self.data[session_id]['metadata']['whitelisted'] = whitelisted
|
||||
self.data[session_id]['metadata']['reason'] = reason
|
||||
metadata = self.data[session_id].get('metadata')
|
||||
if isinstance(metadata, dict):
|
||||
metadata['message_count'] = message_count
|
||||
metadata['errors'] = errors
|
||||
metadata['size_kb'] = size_kb
|
||||
metadata['whitelisted'] = whitelisted
|
||||
metadata['reason'] = reason
|
||||
# self.data[session_id]['metadata']['timestamp'] = datetime.utcnow() # Optionally add a timestamp
|
||||
# Also update the top-level whitelisted flag if provided
|
||||
if whitelisted is not None:
|
||||
@@ -150,7 +153,7 @@ class LogRegistry:
|
||||
if session_data is None:
|
||||
return False # Non-existent sessions are not whitelisted
|
||||
# Check the top-level 'whitelisted' flag. If it's not set or False, it's not whitelisted.
|
||||
return session_data.get('whitelisted', False)
|
||||
return bool(session_data.get('whitelisted', False))
|
||||
|
||||
def update_auto_whitelist_status(self, session_id: str) -> None:
|
||||
"""
|
||||
@@ -165,14 +168,14 @@ class LogRegistry:
|
||||
return
|
||||
session_data = self.data[session_id]
|
||||
session_path = session_data.get('path')
|
||||
if not session_path or not os.path.isdir(session_path):
|
||||
if not session_path or not os.path.isdir(str(session_path)):
|
||||
return
|
||||
total_size_bytes = 0
|
||||
message_count = 0
|
||||
found_keywords = []
|
||||
keywords_to_check = ['ERROR', 'WARNING', 'EXCEPTION']
|
||||
try:
|
||||
for entry in os.scandir(session_path):
|
||||
for entry in os.scandir(str(session_path)):
|
||||
if entry.is_file():
|
||||
size = entry.stat().st_size
|
||||
total_size_bytes += size
|
||||
@@ -210,7 +213,7 @@ class LogRegistry:
|
||||
reason=reason
|
||||
)
|
||||
|
||||
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict]:
|
||||
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Retrieves a list of sessions that are older than a specific cutoff time
|
||||
and are not marked as whitelisted.
|
||||
@@ -240,3 +243,4 @@ class LogRegistry:
|
||||
'start_time': start_time_raw
|
||||
})
|
||||
return old_sessions
|
||||
|
||||
|
||||
142
mcp_client.py
142
mcp_client.py
@@ -31,8 +31,10 @@ so the AI doesn't wander outside the project workspace.
|
||||
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any
|
||||
from typing import Optional, Callable, Any, cast
|
||||
import os
|
||||
import ast
|
||||
import subprocess
|
||||
import summarize
|
||||
import outline_tool
|
||||
import urllib.request
|
||||
@@ -151,7 +153,7 @@ def _resolve_and_check(raw_path: str) -> tuple[Path | None, str]:
|
||||
def read_file(path: str) -> str:
|
||||
"""Return the UTF-8 content of a file, or an error string."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
@@ -165,7 +167,7 @@ def read_file(path: str) -> str:
|
||||
def list_directory(path: str) -> str:
|
||||
"""List entries in a directory. Returns a compact text table."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: path not found: {path}"
|
||||
@@ -195,7 +197,7 @@ def search_files(path: str, pattern: str) -> str:
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.is_dir():
|
||||
return f"ERROR: not a directory: {path}"
|
||||
@@ -226,7 +228,7 @@ def get_file_summary(path: str) -> str:
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
@@ -245,6 +247,7 @@ def py_get_skeleton(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file() or p.suffix != ".py":
|
||||
@@ -264,6 +267,7 @@ def py_get_code_outline(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file():
|
||||
@@ -279,12 +283,13 @@ def get_file_slice(path: str, start_line: int, end_line: int) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
start_idx = int(start_line) - 1
|
||||
end_idx = int(end_line)
|
||||
start_idx = start_line - 1
|
||||
end_idx = end_line
|
||||
return "".join(lines[start_idx:end_idx])
|
||||
except Exception as e:
|
||||
return f"ERROR reading slice from '{path}': {e}"
|
||||
@@ -294,12 +299,13 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
start_idx = int(start_line) - 1
|
||||
end_idx = int(end_line)
|
||||
start_idx = start_line - 1
|
||||
end_idx = end_line
|
||||
if new_content and not new_content.endswith("\n"):
|
||||
new_content += "\n"
|
||||
new_lines = new_content.splitlines(keepends=True) if new_content else []
|
||||
@@ -309,12 +315,11 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
except Exception as e:
|
||||
return f"ERROR updating slice in '{path}': {e}"
|
||||
|
||||
def _get_symbol_node(tree: Any, name: str) -> Any:
|
||||
def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
|
||||
"""Helper to find an AST node by name (Class, Function, or Variable). Supports dot notation."""
|
||||
import ast
|
||||
parts = name.split(".")
|
||||
|
||||
def find_in_scope(scope_node, target_name):
|
||||
def find_in_scope(scope_node: Any, target_name: str) -> Optional[ast.AST]:
|
||||
# scope_node could be Module, ClassDef, or FunctionDef
|
||||
body = getattr(scope_node, "body", [])
|
||||
for node in body:
|
||||
@@ -345,6 +350,7 @@ def py_get_definition(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file():
|
||||
@@ -352,14 +358,13 @@ def py_get_definition(path: str, name: str) -> str:
|
||||
if p.suffix != ".py":
|
||||
return f"ERROR: py_get_definition currently only supports .py files (unsupported: {p.suffix})"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if node:
|
||||
start = getattr(node, "lineno") - 1
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno")) - 1
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return "".join(lines[start:end])
|
||||
return f"ERROR: could not find definition '{name}' in {path}"
|
||||
except Exception as e:
|
||||
@@ -370,17 +375,17 @@ def py_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node:
|
||||
return f"ERROR: could not find definition '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno"))
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return set_file_slice(path, start, end, new_content)
|
||||
except Exception as e:
|
||||
return f"ERROR updating definition '{name}' in '{path}': {e}"
|
||||
@@ -390,17 +395,17 @@ def py_get_signature(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
return f"ERROR: could not find function/method '{name}' in {path}"
|
||||
start = getattr(node, "lineno") - 1
|
||||
start = node.lineno - 1
|
||||
body_start = node.body[0].lineno - 1
|
||||
sig_lines = lines[start:body_start]
|
||||
sig = "".join(sig_lines).strip()
|
||||
@@ -420,17 +425,17 @@ def py_set_signature(path: str, name: str, new_signature: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
return f"ERROR: could not find function/method '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
start = node.lineno
|
||||
body_start_line = node.body[0].lineno
|
||||
# We replace from start until body_start_line - 1
|
||||
# But we must be careful about comments/docstrings between sig and body
|
||||
@@ -445,10 +450,10 @@ def py_get_class_summary(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
@@ -461,7 +466,7 @@ def py_get_class_summary(path: str, name: str) -> str:
|
||||
summary.append(f" Docstring: {doc}")
|
||||
for body_node in node.body:
|
||||
if isinstance(body_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
start = getattr(body_node, "lineno") - 1
|
||||
start = body_node.lineno - 1
|
||||
body_start = body_node.body[0].lineno - 1
|
||||
sig = "".join(lines[start:body_start]).strip()
|
||||
summary.append(f" - {sig}")
|
||||
@@ -474,18 +479,18 @@ def py_get_var_declaration(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.is_file() or p.suffix != ".py":
|
||||
return f"ERROR: not a python file: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
|
||||
return f"ERROR: could not find variable '{name}' in {path}"
|
||||
start = getattr(node, "lineno") - 1
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno")) - 1
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return "".join(lines[start:end])
|
||||
except Exception as e:
|
||||
return f"ERROR retrieving variable '{name}' from '{path}': {e}"
|
||||
@@ -495,17 +500,17 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.is_file() or p.suffix != ".py":
|
||||
return f"ERROR: not a python file: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
|
||||
return f"ERROR: could not find variable '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno"))
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return set_file_slice(path, start, end, new_declaration)
|
||||
except Exception as e:
|
||||
return f"ERROR updating variable '{name}' in '{path}': {e}"
|
||||
@@ -516,10 +521,10 @@ def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str:
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
import subprocess
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
cmd = ["git", "diff", base_rev]
|
||||
if head_rev:
|
||||
cmd.append(head_rev)
|
||||
@@ -536,12 +541,13 @@ def py_find_usages(path: str, name: str) -> str:
|
||||
"""Finds exact string matches of a symbol in a given file or directory."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
try:
|
||||
import re
|
||||
pattern = re.compile(r"\b" + re.escape(name) + r"\b")
|
||||
results = []
|
||||
|
||||
def _search_file(fp):
|
||||
def _search_file(fp: Path) -> None:
|
||||
if fp.name == "history.toml" or fp.name.endswith("_history.toml"): return
|
||||
if not _is_allowed(fp): return
|
||||
try:
|
||||
@@ -573,9 +579,9 @@ def py_get_imports(path: str) -> str:
|
||||
"""Parses a file's AST and returns a strict list of its dependencies."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8")
|
||||
tree = ast.parse(code)
|
||||
imports = []
|
||||
@@ -596,9 +602,9 @@ def py_check_syntax(path: str) -> str:
|
||||
"""Runs a quick syntax check on a Python file."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8")
|
||||
ast.parse(code)
|
||||
return f"Syntax OK: {path}"
|
||||
@@ -611,10 +617,10 @@ def py_get_hierarchy(path: str, class_name: str) -> str:
|
||||
"""Scans the project to find subclasses of a given class."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
import ast
|
||||
subclasses = []
|
||||
assert p is not None
|
||||
subclasses: list[str] = []
|
||||
|
||||
def _search_file(fp):
|
||||
def _search_file(fp: Path) -> None:
|
||||
if not _is_allowed(fp): return
|
||||
try:
|
||||
code = fp.read_text(encoding="utf-8")
|
||||
@@ -625,6 +631,7 @@ def py_get_hierarchy(path: str, class_name: str) -> str:
|
||||
if isinstance(base, ast.Name) and base.id == class_name:
|
||||
subclasses.append(f"{fp.name}: class {node.name}({class_name})")
|
||||
elif isinstance(base, ast.Attribute) and base.attr == class_name:
|
||||
if isinstance(base.value, ast.Name):
|
||||
subclasses.append(f"{fp.name}: class {node.name}({base.value.id}.{class_name})")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -647,9 +654,9 @@ def py_get_docstring(path: str, name: str) -> str:
|
||||
"""Extracts the docstring for a specific module, class, or function."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8")
|
||||
tree = ast.parse(code)
|
||||
if not name or name == "module":
|
||||
@@ -657,8 +664,10 @@ def py_get_docstring(path: str, name: str) -> str:
|
||||
return doc if doc else "No module docstring found."
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node: return f"ERROR: could not find symbol '{name}' in {path}"
|
||||
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
|
||||
doc = ast.get_docstring(node)
|
||||
return doc if doc else f"No docstring found for '{name}'."
|
||||
return f"No docstring found for '{name}'."
|
||||
except Exception as e:
|
||||
return f"ERROR getting docstring for '{name}': {e}"
|
||||
|
||||
@@ -666,12 +675,13 @@ def get_tree(path: str, max_depth: int = 2) -> str:
|
||||
"""Returns a directory structure up to a max depth."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
if not p.is_dir(): return f"ERROR: not a directory: {path}"
|
||||
try:
|
||||
max_depth = int(max_depth)
|
||||
m_depth = max_depth
|
||||
|
||||
def _build_tree(dir_path, current_depth, prefix=""):
|
||||
if current_depth > max_depth: return []
|
||||
def _build_tree(dir_path: Path, current_depth: int, prefix: str = "") -> list[str]:
|
||||
if current_depth > m_depth: return []
|
||||
lines = []
|
||||
try:
|
||||
entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower()))
|
||||
@@ -706,11 +716,11 @@ class _DDGParser(HTMLParser):
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
attrs_dict = dict(attrs)
|
||||
if tag == "a" and "result__url" in attrs_dict.get("class", ""):
|
||||
self.current_link = attrs_dict.get("href", "")
|
||||
if tag == "a" and "result__snippet" in attrs_dict.get("class", ""):
|
||||
if tag == "a" and "result__url" in cast(str, attrs_dict.get("class", "")):
|
||||
self.current_link = cast(str, attrs_dict.get("href", ""))
|
||||
if tag == "a" and "result__snippet" in cast(str, attrs_dict.get("class", "")):
|
||||
self.in_snippet = True
|
||||
if tag == "h2" and "result__title" in attrs_dict.get("class", ""):
|
||||
if tag == "h2" and "result__title" in cast(str, attrs_dict.get("class", "")):
|
||||
self.in_title = True
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
@@ -777,7 +787,9 @@ def fetch_url(url: str) -> str:
|
||||
"""Fetch a URL and return its text content (stripped of HTML tags)."""
|
||||
# Correct duckduckgo redirect links if passed
|
||||
if url.startswith("//duckduckgo.com/l/?uddg="):
|
||||
url = urllib.parse.unquote(url.split("uddg=")[1].split("&")[0])
|
||||
split_uddg = url.split("uddg=")
|
||||
if len(split_uddg) > 1:
|
||||
url = urllib.parse.unquote(split_uddg[1].split("&")[0])
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'})
|
||||
@@ -819,13 +831,13 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
"""
|
||||
# Handle aliases
|
||||
path = tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", "")))
|
||||
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
|
||||
if tool_name == "read_file":
|
||||
return read_file(path)
|
||||
if tool_name == "list_directory":
|
||||
return list_directory(path)
|
||||
if tool_name == "search_files":
|
||||
return search_files(path, tool_input.get("pattern", "*"))
|
||||
return search_files(path, str(tool_input.get("pattern", "*")))
|
||||
if tool_name == "get_file_summary":
|
||||
return get_file_summary(path)
|
||||
if tool_name == "py_get_skeleton":
|
||||
@@ -833,47 +845,47 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
if tool_name == "py_get_code_outline":
|
||||
return py_get_code_outline(path)
|
||||
if tool_name == "py_get_definition":
|
||||
return py_get_definition(path, tool_input.get("name", ""))
|
||||
return py_get_definition(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_update_definition":
|
||||
return py_update_definition(path, tool_input.get("name", ""), tool_input.get("new_content", ""))
|
||||
return py_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", "")))
|
||||
if tool_name == "py_get_signature":
|
||||
return py_get_signature(path, tool_input.get("name", ""))
|
||||
return py_get_signature(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_set_signature":
|
||||
return py_set_signature(path, tool_input.get("name", ""), tool_input.get("new_signature", ""))
|
||||
return py_set_signature(path, str(tool_input.get("name", "")), str(tool_input.get("new_signature", "")))
|
||||
if tool_name == "py_get_class_summary":
|
||||
return py_get_class_summary(path, tool_input.get("name", ""))
|
||||
return py_get_class_summary(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_get_var_declaration":
|
||||
return py_get_var_declaration(path, tool_input.get("name", ""))
|
||||
return py_get_var_declaration(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_set_var_declaration":
|
||||
return py_set_var_declaration(path, tool_input.get("name", ""), tool_input.get("new_declaration", ""))
|
||||
return py_set_var_declaration(path, str(tool_input.get("name", "")), str(tool_input.get("new_declaration", "")))
|
||||
if tool_name == "get_file_slice":
|
||||
return get_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1))
|
||||
return get_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)))
|
||||
if tool_name == "set_file_slice":
|
||||
return set_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1), tool_input.get("new_content", ""))
|
||||
return set_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)), str(tool_input.get("new_content", "")))
|
||||
if tool_name == "get_git_diff":
|
||||
return get_git_diff(
|
||||
path,
|
||||
tool_input.get("base_rev", "HEAD"),
|
||||
tool_input.get("head_rev", "")
|
||||
str(tool_input.get("base_rev", "HEAD")),
|
||||
str(tool_input.get("head_rev", ""))
|
||||
)
|
||||
if tool_name == "web_search":
|
||||
return web_search(tool_input.get("query", ""))
|
||||
return web_search(str(tool_input.get("query", "")))
|
||||
if tool_name == "fetch_url":
|
||||
return fetch_url(tool_input.get("url", ""))
|
||||
return fetch_url(str(tool_input.get("url", "")))
|
||||
if tool_name == "get_ui_performance":
|
||||
return get_ui_performance()
|
||||
if tool_name == "py_find_usages":
|
||||
return py_find_usages(path, tool_input.get("name", ""))
|
||||
return py_find_usages(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_get_imports":
|
||||
return py_get_imports(path)
|
||||
if tool_name == "py_check_syntax":
|
||||
return py_check_syntax(path)
|
||||
if tool_name == "py_get_hierarchy":
|
||||
return py_get_hierarchy(path, tool_input.get("class_name", ""))
|
||||
return py_get_hierarchy(path, str(tool_input.get("class_name", "")))
|
||||
if tool_name == "py_get_docstring":
|
||||
return py_get_docstring(path, tool_input.get("name", ""))
|
||||
return py_get_docstring(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "get_tree":
|
||||
return get_tree(path, tool_input.get("max_depth", 2))
|
||||
return get_tree(path, int(tool_input.get("max_depth", 2)))
|
||||
return f"ERROR: unknown MCP tool '{tool_name}'"
|
||||
# ------------------------------------------------------------------ tool schema helpers
|
||||
# These are imported by ai_client.py to build provider-specific declarations.
|
||||
|
||||
12
models.py
12
models.py
@@ -103,9 +103,9 @@ class WorkerContext:
|
||||
class Metadata:
|
||||
id: str
|
||||
name: str
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
status: Optional[str] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
@@ -121,9 +121,9 @@ class Metadata:
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
status=data.get("status", "todo"),
|
||||
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else datetime.now(),
|
||||
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else datetime.now(),
|
||||
status=data.get("status"),
|
||||
created_at=datetime.fromisoformat(data['created_at']) if data.get('created_at') else None,
|
||||
updated_at=datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else None,
|
||||
)
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -13,13 +13,14 @@ class CodeOutliner:
|
||||
return f"ERROR parsing code: {e}"
|
||||
output = []
|
||||
|
||||
def get_docstring(node):
|
||||
def get_docstring(node: ast.AST) -> str | None:
|
||||
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
|
||||
doc = ast.get_docstring(node)
|
||||
if doc:
|
||||
return doc.splitlines()[0]
|
||||
return None
|
||||
|
||||
def walk(node, indent=0):
|
||||
def walk(node: ast.AST, indent: int = 0) -> None:
|
||||
if isinstance(node, ast.ClassDef):
|
||||
start_line = node.lineno
|
||||
end_line = getattr(node, "end_lineno", start_line)
|
||||
|
||||
@@ -2,38 +2,38 @@ from __future__ import annotations
|
||||
import time
|
||||
import psutil
|
||||
import threading
|
||||
from typing import Any
|
||||
from typing import Any, Optional, Callable
|
||||
|
||||
class PerformanceMonitor:
|
||||
def __init__(self) -> None:
|
||||
self._start_time = None
|
||||
self._last_frame_time = 0.0
|
||||
self._fps = 0.0
|
||||
self._last_calculated_fps = 0.0
|
||||
self._frame_count = 0
|
||||
self._total_frame_count = 0
|
||||
self._fps_last_time = time.time()
|
||||
self._process = psutil.Process()
|
||||
self._cpu_usage = 0.0
|
||||
self._cpu_lock = threading.Lock()
|
||||
self._start_time: Optional[float] = None
|
||||
self._last_frame_time: float = 0.0
|
||||
self._fps: float = 0.0
|
||||
self._last_calculated_fps: float = 0.0
|
||||
self._frame_count: int = 0
|
||||
self._total_frame_count: int = 0
|
||||
self._fps_last_time: float = time.time()
|
||||
self._process: psutil.Process = psutil.Process()
|
||||
self._cpu_usage: float = 0.0
|
||||
self._cpu_lock: threading.Lock = threading.Lock()
|
||||
# Input lag tracking
|
||||
self._last_input_time = None
|
||||
self._input_lag_ms = 0.0
|
||||
self._last_input_time: Optional[float] = None
|
||||
self._input_lag_ms: float = 0.0
|
||||
# Alerts
|
||||
self.alert_callback = None
|
||||
self.thresholds = {
|
||||
self.alert_callback: Optional[Callable[[str], None]] = None
|
||||
self.thresholds: dict[str, float] = {
|
||||
'frame_time_ms': 33.3, # < 30 FPS
|
||||
'cpu_percent': 80.0,
|
||||
'input_lag_ms': 100.0
|
||||
}
|
||||
self._last_alert_time = 0
|
||||
self._alert_cooldown = 30 # seconds
|
||||
self._last_alert_time: float = 0.0
|
||||
self._alert_cooldown: int = 30 # seconds
|
||||
# Detailed profiling
|
||||
self._component_timings = {}
|
||||
self._comp_start = {}
|
||||
self._component_timings: dict[str, float] = {}
|
||||
self._comp_start: dict[str, float] = {}
|
||||
# Start CPU usage monitoring thread
|
||||
self._stop_event = threading.Event()
|
||||
self._cpu_thread = threading.Thread(target=self._monitor_cpu, daemon=True)
|
||||
self._stop_event: threading.Event = threading.Event()
|
||||
self._cpu_thread: threading.Thread = threading.Thread(target=self._monitor_cpu, daemon=True)
|
||||
self._cpu_thread.start()
|
||||
|
||||
def _monitor_cpu(self) -> None:
|
||||
@@ -107,15 +107,13 @@ class PerformanceMonitor:
|
||||
def get_metrics(self) -> dict[str, Any]:
|
||||
with self._cpu_lock:
|
||||
cpu_usage = self._cpu_usage
|
||||
metrics = {
|
||||
metrics: dict[str, Any] = {
|
||||
'last_frame_time_ms': self._last_frame_time,
|
||||
'fps': self._last_calculated_fps,
|
||||
'cpu_percent': cpu_usage,
|
||||
'total_frames': self._total_frame_count,
|
||||
'input_lag_ms': self._last_input_time if self._last_input_time else 0.0 # Wait, this should be the calculated lag
|
||||
'input_lag_ms': self._input_lag_ms
|
||||
}
|
||||
# Oops, fixed the input lag logic in previous turn, let's keep it consistent
|
||||
metrics['input_lag_ms'] = self._input_lag_ms
|
||||
# Add detailed timings
|
||||
for name, elapsed in self._component_timings.items():
|
||||
metrics[f'time_{name}_ms'] = elapsed
|
||||
|
||||
@@ -8,5 +8,5 @@ active = "main"
|
||||
|
||||
[discussions.main]
|
||||
git_commit = ""
|
||||
last_updated = "2026-03-04T00:55:41"
|
||||
last_updated = "2026-03-04T09:44:10"
|
||||
history = []
|
||||
|
||||
@@ -37,4 +37,28 @@ disallow_incomplete_defs = true
|
||||
ignore_missing_imports = true
|
||||
explicit_package_bases = true
|
||||
|
||||
[tool.ruff]
|
||||
# Target version
|
||||
target-version = "py311"
|
||||
exclude = [
|
||||
"scripts/ai_style_formatter.py",
|
||||
"scripts/temp_def.py",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
# Enable standard rules
|
||||
select = ["E", "F", "W"]
|
||||
# Ignore style choices that conflict with project's compact style
|
||||
ignore = [
|
||||
"E701", # Multiple statements on one line (colon)
|
||||
"E702", # Multiple statements on one line (semicolon)
|
||||
"E402", # Module level import not at top of file
|
||||
"E722", # Do not use bare `except`
|
||||
"E501", # Line too long
|
||||
"W291", # Trailing whitespace
|
||||
"W293", # Blank line contains whitespace
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
|
||||
ps1_path: Optional[Path] = _SCRIPTS_DIR / ps1_name
|
||||
|
||||
try:
|
||||
if ps1_path:
|
||||
ps1_path.write_text(script, encoding="utf-8")
|
||||
except Exception as exc:
|
||||
ps1_path = None
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
if not client.wait_for_server(timeout=5):
|
||||
print("Hook server not found. Start GUI with --enable-test-hooks")
|
||||
return
|
||||
sim_agent = UserSimAgent(client)
|
||||
UserSimAgent(client)
|
||||
# 1. Reset session to start clean
|
||||
print("Resetting session...")
|
||||
client.click("btn_reset")
|
||||
|
||||
@@ -26,7 +26,7 @@ context block that replaces full file contents in the initial <context> send.
|
||||
import ast
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import Callable, Any
|
||||
|
||||
# ------------------------------------------------------------------ per-type extractors
|
||||
|
||||
@@ -160,7 +160,7 @@ def summarise_file(path: Path, content: str) -> str:
|
||||
except Exception as e:
|
||||
return f"_Summariser error: {e}_"
|
||||
|
||||
def summarise_items(file_items: list[dict]) -> list[dict]:
|
||||
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Given a list of file_item dicts (as returned by aggregate.build_file_items),
|
||||
return a parallel list of dicts with an added `summary` key.
|
||||
@@ -178,7 +178,7 @@ def summarise_items(file_items: list[dict]) -> list[dict]:
|
||||
result.append({**item, "summary": summary})
|
||||
return result
|
||||
|
||||
def build_summary_markdown(file_items: list[dict]) -> str:
|
||||
def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build a compact markdown string of file summaries, suitable for the
|
||||
initial <context> block instead of full file contents.
|
||||
|
||||
@@ -10,7 +10,7 @@ import datetime
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Generator, Any
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure project root is in path for imports
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
@@ -41,7 +41,7 @@ class VerificationLogger:
|
||||
})
|
||||
|
||||
def finalize(self, title: str, status: str, result_msg: str) -> None:
|
||||
elapsed = round(time.time() - self.start_time, 2)
|
||||
round(time.time() - self.start_time, 2)
|
||||
log_file = self.logs_dir / f"{self.script_name}.txt"
|
||||
with open(log_file, "w", encoding="utf-8") as f:
|
||||
f.write(f"[ Test: {self.test_name} ]\n")
|
||||
|
||||
@@ -60,7 +60,6 @@ def main() -> None:
|
||||
return
|
||||
|
||||
# 2. Check for multi-round tool triggers
|
||||
is_resume_list = is_resume and 'list_directory' in prompt
|
||||
is_resume_read = is_resume and 'read_file' in prompt
|
||||
is_resume_powershell = is_resume and 'run_powershell' in prompt
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
import ai_client
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
import ai_client
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure project root is in path for imports
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
@@ -3,7 +3,6 @@ Tests for architecture_boundary_hardening_20260302 — Phase 2.
|
||||
Tasks 2.1-2.4: MCP tool config exposure + MUTATING_TOOLS + HITL enforcement.
|
||||
"""
|
||||
import tomllib
|
||||
import pytest
|
||||
from project_manager import default_project
|
||||
|
||||
MUTATING_TOOLS = {"set_file_slice", "py_update_definition", "py_set_signature", "py_set_var_declaration"}
|
||||
@@ -101,9 +100,8 @@ def test_mutating_tools_excludes_read_tools():
|
||||
|
||||
def test_mutating_tool_triggers_pre_tool_callback(monkeypatch):
|
||||
"""When a mutating tool is called and pre_tool_callback is set, it must be invoked."""
|
||||
import ai_client
|
||||
import mcp_client
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
callback_called = []
|
||||
def fake_callback(desc, base_dir, qa_cb):
|
||||
callback_called.append(desc)
|
||||
@@ -113,12 +111,11 @@ def test_mutating_tool_triggers_pre_tool_callback(monkeypatch):
|
||||
tool_name = "set_file_slice"
|
||||
args = {"path": "foo.py", "start_line": 1, "end_line": 2, "new_content": "x"}
|
||||
# Simulate the logic from all 4 provider dispatch blocks
|
||||
out = ""
|
||||
_res = fake_callback(f"# MCP MUTATING TOOL: {tool_name}", ".", None)
|
||||
if _res is None:
|
||||
out = "USER REJECTED: tool execution cancelled"
|
||||
pass
|
||||
else:
|
||||
out = mcp_client.dispatch(tool_name, args)
|
||||
mcp_client.dispatch(tool_name, args)
|
||||
assert len(callback_called) == 1, "pre_tool_callback must be called for mutating tools"
|
||||
assert mock_dispatch.called
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from models import Ticket
|
||||
from dag_engine import TrackDAG, ExecutionEngine
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from models import Ticket
|
||||
from dag_engine import TrackDAG, ExecutionEngine
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Ensure the project root is in sys.path to resolve imports correctly
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_gemini_cli_parameter_resilience(live_gui: Any) -> None:
|
||||
bridge_path_str = bridge_path.replace("\\", "/")
|
||||
else:
|
||||
bridge_path_str = bridge_path
|
||||
with open(alias_tool_content := "tests/mock_alias_tool.py", "w") as f:
|
||||
with open("tests/mock_alias_tool.py", "w") as f:
|
||||
f.write(f'''import sys, json, os, subprocess
|
||||
prompt = sys.stdin.read()
|
||||
if '"role": "tool"' in prompt:
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from typing import Generator
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from gui_2 import App
|
||||
|
||||
def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from typing import Generator
|
||||
from gui_2 import App
|
||||
import ai_client
|
||||
from events import EventEmitter
|
||||
|
||||
def test_mcp_tool_call_is_dispatched(app_instance: App) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
from typing import Any
|
||||
@@ -7,7 +5,6 @@ from typing import Any
|
||||
# Ensure project root is in path for imports
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from gui_2 import App
|
||||
|
||||
def test_diagnostics_panel_initialization(app_instance: Any) -> None:
|
||||
assert "Diagnostics" in app_instance.show_windows
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from typing import Generator, Any
|
||||
from gui_2 import App
|
||||
import ai_client
|
||||
|
||||
# Ensure project root is in path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
def test_gui_updates_on_event(app_instance: App) -> None:
|
||||
mock_stats = {"percentage": 50.0, "current": 500, "limit": 1000}
|
||||
app_instance.last_md = "mock_md"
|
||||
with patch.object(app_instance, '_refresh_api_metrics') as mock_refresh:
|
||||
# Simulate event (bypassing events.emit since _init_ai_and_hooks is mocked)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from gui_2 import App
|
||||
|
||||
def test_track_proposal_editing(app_instance):
|
||||
# Setup some proposed tracks
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from gui_2 import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
import sys
|
||||
import os
|
||||
@@ -41,7 +40,7 @@ def test_performance_history_updates(app_instance: Any) -> None:
|
||||
def test_gui_updates_on_event(app_instance: App) -> None:
|
||||
mock_stats = {"percentage": 50.0, "current": 500, "limit": 1000}
|
||||
app_instance.last_md = "mock_md"
|
||||
with patch('ai_client.get_token_stats', return_value=mock_stats) as mock_get_stats:
|
||||
with patch('ai_client.get_token_stats', return_value=mock_stats):
|
||||
app_instance._on_api_event(payload={"text": "test"})
|
||||
app_instance._process_pending_gui_tasks()
|
||||
assert app_instance._token_stats["percentage"] == 50.0
|
||||
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import patch
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from api_hook_client import ApiHookClient
|
||||
import gui_2
|
||||
|
||||
def test_hooks_enabled_via_cli(mock_app) -> None:
|
||||
with patch.object(sys, 'argv', ['gui_2.py', '--enable-test-hooks']):
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
Tests for mma_agent_focus_ux_20260302 — Phase 1: Tier Tagging at Emission.
|
||||
These tests are written RED-first: they fail before implementation.
|
||||
"""
|
||||
from typing import Generator
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
import ai_client
|
||||
from gui_2 import App
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
"""
|
||||
Tests for mma_agent_focus_ux_20260302 — Phase 3: Focus Agent UI + Filter Logic.
|
||||
"""
|
||||
from typing import Generator
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from gui_2 import App
|
||||
|
||||
|
||||
def test_ui_focus_agent_state_var_exists(app_instance):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
import time
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from typing import Generator
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
from gui_2 import App
|
||||
|
||||
def test_cb_ticket_retry(app_instance: App) -> None:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"""Tests for context & token visualization (Track: context_token_viz_20260301)."""
|
||||
from typing import Generator
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
import ai_client
|
||||
from ai_client import _add_bleed_derived, get_history_bleed_stats
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import time
|
||||
from api_hook_client import ApiHookClient
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
|
||||
def test_vlogger_available(vlogger):
|
||||
vlogger.log_state("Test", "Before", "After")
|
||||
|
||||
11
theme.py
11
theme.py
@@ -22,13 +22,14 @@ Usage
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
# Colour key names match the DPG mvThemeCol_* constants (string lookup below).
|
||||
# Only keys that differ from DPG defaults need to be listed.
|
||||
|
||||
_PALETTES: dict[str, dict] = {
|
||||
_PALETTES: dict[str, dict[str, Any]] = {
|
||||
"DPG Default": {}, # empty = reset to DPG built-in defaults
|
||||
"10x Dark": {
|
||||
# Window / frame chrome
|
||||
@@ -285,11 +286,11 @@ def get_current_font_size() -> float:
|
||||
def get_current_scale() -> float:
|
||||
return _current_scale
|
||||
|
||||
def get_palette_colours(name: str) -> dict:
|
||||
def get_palette_colours(name: str) -> dict[str, Any]:
|
||||
"""Return a copy of the colour dict for the named palette."""
|
||||
return dict(_PALETTES.get(name, {}))
|
||||
|
||||
def apply(palette_name: str, overrides: dict | None = None) -> None:
|
||||
def apply(palette_name: str, overrides: dict[str, Any] | None = None) -> None:
|
||||
"""
|
||||
Build a global DPG theme from the named palette plus optional per-colour
|
||||
overrides, and bind it as the default theme.
|
||||
@@ -368,7 +369,7 @@ def set_scale(factor: float) -> None:
|
||||
_current_scale = factor
|
||||
dpg.set_global_font_scale(factor)
|
||||
|
||||
def save_to_config(config: dict) -> None:
|
||||
def save_to_config(config: dict[str, Any]) -> None:
|
||||
"""Persist theme settings into the config dict under [theme]."""
|
||||
config.setdefault("theme", {})
|
||||
config["theme"]["palette"] = _current_palette
|
||||
@@ -376,7 +377,7 @@ def save_to_config(config: dict) -> None:
|
||||
config["theme"]["font_size"] = _current_font_size
|
||||
config["theme"]["scale"] = _current_scale
|
||||
|
||||
def load_from_config(config: dict) -> None:
|
||||
def load_from_config(config: dict[str, Any]) -> None:
|
||||
"""Read [theme] from config and apply everything."""
|
||||
t = config.get("theme", {})
|
||||
palette = t.get("palette", "DPG Default")
|
||||
|
||||
Reference in New Issue
Block a user