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