refactor(sdm): Refine SDM tags to 'External Only' and update core files. Pruned internal references to conserve tokens.
This commit is contained in:
@@ -15,3 +15,4 @@ dpg_layout.ini
|
||||
tests/temp_workspace
|
||||
.mypy_cache
|
||||
.slop_cache
|
||||
sdm_report_refined.json
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
|
||||
def find_closing_quotes_pos(line: str) -> Tuple[int, str]:
|
||||
pos_double = line.rfind('"""')
|
||||
pos_single = line.rfind("'''")
|
||||
if pos_double != -1 and pos_single != -1:
|
||||
if pos_double > pos_single: return pos_double, '"""'
|
||||
else: return pos_single, "'''"
|
||||
elif pos_double != -1:
|
||||
return pos_double, '"""'
|
||||
elif pos_single != -1:
|
||||
return pos_single, "'''"
|
||||
return -1, ""
|
||||
|
||||
class SdmDocstringInjectorVisitor(ast.NodeVisitor):
|
||||
def __init__(self, file_path: str, sdm_tags_map: Dict[str, Any], lines: List[str]):
|
||||
self.file_path = file_path
|
||||
self.sdm_tags_map = sdm_tags_map
|
||||
self.lines = lines
|
||||
self.targets_to_modify = []
|
||||
self.current_class_name = None
|
||||
self.project_root = pathlib.Path.cwd().resolve()
|
||||
|
||||
def get_rel_path(self, path):
|
||||
p = pathlib.Path(path).resolve()
|
||||
try:
|
||||
return str(p.relative_to(self.project_root)).replace("\\", "/")
|
||||
except (ValueError, RuntimeError):
|
||||
return str(p).replace("\\", "/")
|
||||
|
||||
def _get_sdm_tags(self, name: str, node_type: str, parent_class_name: Optional[str] = None) -> List[str]:
|
||||
relative_file_path = self.get_rel_path(self.file_path)
|
||||
file_data = self.sdm_tags_map.get(relative_file_path)
|
||||
if not file_data: return []
|
||||
tags = []
|
||||
if node_type == 'ClassDef':
|
||||
class_data = file_data.get('classes', {}).get(name, {})
|
||||
class_tag = class_data.get('class_tag')
|
||||
if class_tag: tags.append(class_tag)
|
||||
elif node_type in ('FunctionDef', 'AsyncFunctionDef'):
|
||||
if parent_class_name:
|
||||
class_data = file_data.get('classes', {}).get(parent_class_name, {})
|
||||
tag = class_data.get('methods', {}).get(name)
|
||||
if tag: tags.append(tag)
|
||||
else:
|
||||
tag = file_data.get('functions', {}).get(name)
|
||||
if tag: tags.append(tag)
|
||||
return tags
|
||||
|
||||
def _process_node(self, node, node_type: str):
|
||||
if not node.body: return
|
||||
sdm_tags = self._get_sdm_tags(node.name, node_type, self.current_class_name)
|
||||
first_body_node = node.body[0]
|
||||
if (node.lineno == first_body_node.lineno): return
|
||||
|
||||
docstring_node = None
|
||||
if isinstance(node.body[0], ast.Expr) and \
|
||||
isinstance(node.body[0].value, ast.Constant) and isinstance(node.body[0].value.value, str):
|
||||
docstring_node = node.body[0].value
|
||||
|
||||
# Use col_offset of the first body node for exact matching
|
||||
body_indent_count = first_body_node.col_offset
|
||||
|
||||
if docstring_node:
|
||||
self.targets_to_modify.append({
|
||||
'type': 'append', 'node': node, 'name': node.name, 'sdm_tags': sdm_tags,
|
||||
'start_lineno': docstring_node.lineno, 'end_lineno': docstring_node.end_lineno,
|
||||
'indent_count': body_indent_count, 'existing_doc': docstring_node.value
|
||||
})
|
||||
elif sdm_tags:
|
||||
self.targets_to_modify.append({
|
||||
'type': 'new', 'node': node, 'name': node.name, 'sdm_tags': sdm_tags,
|
||||
'insert_lineno': first_body_node.lineno, 'indent_count': body_indent_count
|
||||
})
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self._process_node(node, 'ClassDef')
|
||||
old_class = self.current_class_name
|
||||
self.current_class_name = node.name
|
||||
self.generic_visit(node)
|
||||
self.current_class_name = old_class
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self._process_node(node, 'FunctionDef')
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self._process_node(node, 'AsyncFunctionDef')
|
||||
self.generic_visit(node)
|
||||
|
||||
def strip_tags(docstring: str) -> str:
|
||||
lines = docstring.splitlines()
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if re.search(r'\[C:.*\]|\[M:.*\]|\[U:.*\]|\[VARS:.*\]', line): continue
|
||||
new_lines.append(line)
|
||||
while new_lines and not new_lines[-1].strip(): new_lines.pop()
|
||||
return "\n".join(new_lines)
|
||||
|
||||
def process_file(py_file_path: pathlib.Path, sdm_tags_map):
|
||||
try:
|
||||
with open(py_file_path, 'r', encoding='utf-8') as f: content = f.read()
|
||||
lines = content.splitlines()
|
||||
if not lines: return
|
||||
try: tree = ast.parse(content)
|
||||
except SyntaxError: return
|
||||
visitor = SdmDocstringInjectorVisitor(str(py_file_path.resolve()), sdm_tags_map, lines)
|
||||
visitor.visit(tree)
|
||||
if not visitor.targets_to_modify: return
|
||||
visitor.targets_to_modify.sort(key=lambda t: t['node'].lineno, reverse=True)
|
||||
modified_lines = lines[:]
|
||||
file_modified = False
|
||||
for target in visitor.targets_to_modify:
|
||||
sdm_tags = target['sdm_tags']
|
||||
indent = " " * target['indent_count']
|
||||
if target['type'] == 'append':
|
||||
clean_doc = strip_tags(target['existing_doc'])
|
||||
if sdm_tags:
|
||||
prepared_tags = [f"{indent}{line}" for t in sdm_tags for line in t.splitlines()]
|
||||
new_content = (clean_doc + "\n" + "\n".join(prepared_tags)) if clean_doc.strip() else "\n".join(prepared_tags)
|
||||
else:
|
||||
new_content = clean_doc
|
||||
start_idx = target['start_lineno'] - 1
|
||||
end_idx = target['end_lineno'] - 1
|
||||
first_line, last_line = modified_lines[start_idx], modified_lines[end_idx]
|
||||
q_start_pos = first_line.find('"""')
|
||||
if q_start_pos == -1: q_start_pos = first_line.find("'''")
|
||||
q_end_pos, q_type = find_closing_quotes_pos(last_line)
|
||||
if q_start_pos != -1 and q_end_pos != -1:
|
||||
q_prefix, q_suffix = first_line[:q_start_pos + 3], last_line[q_end_pos:]
|
||||
if "\n" in new_content or (start_idx != end_idx):
|
||||
replacement = [q_prefix] + [f"{indent}{l}" for l in new_content.splitlines()] + [f"{indent}{q_suffix}"]
|
||||
else:
|
||||
replacement = [f"{q_prefix}{new_content}{q_suffix}"]
|
||||
modified_lines[start_idx:end_idx+1] = replacement
|
||||
file_modified = True
|
||||
elif sdm_tags:
|
||||
prepared_tags = [f"{indent}{line}" for t in sdm_tags for line in t.splitlines()]
|
||||
new_doc = [f'{indent}"""', "\n".join(prepared_tags), f'{indent}"""']
|
||||
insert_idx = target['insert_lineno'] - 1
|
||||
while insert_idx > 0 and not modified_lines[insert_idx-1].strip(): insert_idx -= 1
|
||||
modified_lines[insert_idx:insert_idx] = new_doc
|
||||
file_modified = True
|
||||
if file_modified:
|
||||
with open(py_file_path, 'w', encoding='utf-8') as f: f.write("\n".join(modified_lines))
|
||||
except Exception as e: print(f"Error processing {py_file_path}: {e}", file=sys.stderr)
|
||||
|
||||
def main():
|
||||
sdm_report_path = "sdm_report_refined.json"
|
||||
if not pathlib.Path(sdm_report_path).exists():
|
||||
print(f"Error: {sdm_report_path} not found.", file=sys.stderr); sys.exit(1)
|
||||
with open(sdm_report_path, 'r', encoding='utf-8') as f: sdm_tags_map = json.load(f)
|
||||
targets = sys.argv[1:]
|
||||
if not targets:
|
||||
for d in ["src", "simulation", "tests"]:
|
||||
sd = pathlib.Path(d)
|
||||
if sd.exists():
|
||||
for f in sd.rglob("*.py"): process_file(f, sdm_tags_map)
|
||||
else:
|
||||
for t in targets:
|
||||
tp = pathlib.Path(t)
|
||||
if tp.is_file(): process_file(tp, sdm_tags_map)
|
||||
elif tp.is_dir():
|
||||
for f in tp.rglob("*.py"): process_file(f, sdm_tags_map)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,206 @@
|
||||
import ast
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
class SDMMapper:
|
||||
def __init__(self):
|
||||
self.files = {} # path -> {"functions": {}, "classes": {}}
|
||||
self.functions_global = {} # name -> {"file": str, "class": str, "callers": set()}
|
||||
self.current_file = ""
|
||||
self.current_class = None
|
||||
self.current_function = None
|
||||
self.project_root = pathlib.Path.cwd().resolve()
|
||||
|
||||
def get_rel_path(self, path):
|
||||
p = pathlib.Path(path).resolve()
|
||||
try:
|
||||
return str(p.relative_to(self.project_root)).replace("\\", "/")
|
||||
except (ValueError, RuntimeError):
|
||||
return str(p).replace("\\", "/")
|
||||
|
||||
def collect_symbols(self, dirs):
|
||||
for d in dirs:
|
||||
if not os.path.exists(d): continue
|
||||
for root, _, files in os.walk(d):
|
||||
for f in files:
|
||||
if f.endswith(".py"):
|
||||
path = os.path.join(root, f)
|
||||
rel_path = self.get_rel_path(path)
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8-sig") as file:
|
||||
tree = ast.parse(file.read(), filename=path)
|
||||
if rel_path not in self.files:
|
||||
self.files[rel_path] = {"functions": {}, "classes": {}}
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
full_name = node.name
|
||||
# In first pass, we just note definitions.
|
||||
# Class-member identification happens in visit_ClassDef.
|
||||
pass
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
if node.name not in self.files[rel_path]["classes"]:
|
||||
self.files[rel_path]["classes"][node.name] = {"methods": {}, "variables": {}}
|
||||
except Exception as e:
|
||||
print(f"Error collecting symbols from {path}: {e}", file=sys.stderr)
|
||||
|
||||
def analyze_files(self, dirs):
|
||||
for d in dirs:
|
||||
if not os.path.exists(d): continue
|
||||
for root, _, files in os.walk(d):
|
||||
for f in files:
|
||||
if f.endswith(".py"):
|
||||
self.analyze_file(os.path.join(root, f))
|
||||
|
||||
def analyze_file(self, path):
|
||||
self.current_file = self.get_rel_path(path)
|
||||
if self.current_file not in self.files:
|
||||
self.files[self.current_file] = {"functions": {}, "classes": {}}
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8-sig") as file:
|
||||
tree = ast.parse(file.read(), filename=path)
|
||||
visitor = SDMVisitor(self)
|
||||
visitor.visit(tree)
|
||||
except Exception as e:
|
||||
print(f"Error analyzing {path}: {e}", file=sys.stderr)
|
||||
|
||||
class SDMVisitor(ast.NodeVisitor):
|
||||
def __init__(self, mapper):
|
||||
self.mapper = mapper
|
||||
self.current_class = None
|
||||
self.current_function = None
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
old_class = self.current_class
|
||||
self.current_class = node.name
|
||||
if self.current_class not in self.mapper.files[self.mapper.current_file]["classes"]:
|
||||
self.mapper.files[self.mapper.current_file]["classes"][self.current_class] = {"methods": {}, "variables": {}}
|
||||
self.generic_visit(node)
|
||||
self.current_class = old_class
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
old_func = self.current_function
|
||||
self.current_function = node.name
|
||||
|
||||
full_name = f"{self.current_class}.{node.name}" if self.current_class else node.name
|
||||
if full_name not in self.mapper.functions_global:
|
||||
self.mapper.functions_global[full_name] = {
|
||||
"file": self.mapper.current_file,
|
||||
"class": self.current_class,
|
||||
"callers": set()
|
||||
}
|
||||
|
||||
self.generic_visit(node)
|
||||
self.current_function = old_func
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self.visit_FunctionDef(node)
|
||||
|
||||
def visit_Call(self, node):
|
||||
name = None
|
||||
if isinstance(node.func, ast.Name):
|
||||
name = node.func.id
|
||||
elif isinstance(node.func, ast.Attribute):
|
||||
name = node.func.attr
|
||||
|
||||
if name:
|
||||
# Try to find if it's a known function/method
|
||||
potential_matches = [n for n in self.mapper.functions_global if n == name or n.endswith("." + name)]
|
||||
for match in potential_matches:
|
||||
match_file = self.mapper.functions_global[match]["file"]
|
||||
# EXTERNAL FILTER: Only add caller if it's from a different file
|
||||
if match_file != self.mapper.current_file:
|
||||
caller_name = f"{self.current_class}.{self.current_function}" if self.current_class else (self.current_function or "module")
|
||||
# Include file name for external clarity
|
||||
self.mapper.functions_global[match]["callers"].add(f"{self.mapper.current_file}:{caller_name}")
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
if isinstance(node.value, ast.Name) and node.value.id == "self" and self.current_class:
|
||||
attr_name = node.attr
|
||||
class_data = self.mapper.files[self.mapper.current_file]["classes"][self.current_class]
|
||||
if attr_name not in class_data["variables"]:
|
||||
class_data["variables"][attr_name] = {"mutations": [], "usages": set()}
|
||||
|
||||
if isinstance(node.ctx, ast.Store):
|
||||
class_data["variables"][attr_name]["mutations"].append({
|
||||
"file": self.mapper.current_file,
|
||||
"line": node.lineno,
|
||||
"method": self.current_function
|
||||
})
|
||||
elif isinstance(node.ctx, ast.Load):
|
||||
class_data["variables"][attr_name]["usages"].add(self.mapper.current_file)
|
||||
self.generic_visit(node)
|
||||
|
||||
def main():
|
||||
target = "."
|
||||
if len(sys.argv) > 1:
|
||||
target = sys.argv[1]
|
||||
|
||||
mapper = SDMMapper()
|
||||
dirs = ["src", "simulation", "tests"]
|
||||
|
||||
if os.path.isfile(target):
|
||||
mapper.collect_symbols(dirs)
|
||||
mapper.analyze_file(target)
|
||||
else:
|
||||
search_dirs = [target] if target in dirs else dirs
|
||||
mapper.collect_symbols(search_dirs)
|
||||
mapper.analyze_files(search_dirs)
|
||||
|
||||
# Build the final grouped report
|
||||
report = {}
|
||||
|
||||
# 1. Add functions/methods
|
||||
for full_name, data in mapper.functions_global.items():
|
||||
f_path = data["file"]
|
||||
if f_path not in report: report[f_path] = {"functions": {}, "classes": {}}
|
||||
|
||||
# External callers only
|
||||
callers = sorted(list(data["callers"]))
|
||||
if not callers:
|
||||
continue
|
||||
|
||||
tag = f"[C: {', '.join(callers)}]"
|
||||
if data["class"]:
|
||||
c_name = data["class"]
|
||||
if c_name not in report[f_path]["classes"]:
|
||||
report[f_path]["classes"][c_name] = {"methods": {}, "variables": {}}
|
||||
m_name = full_name.split(".")[-1]
|
||||
report[f_path]["classes"][c_name]["methods"][m_name] = tag
|
||||
else:
|
||||
report[f_path]["functions"][full_name] = tag
|
||||
|
||||
# 2. Add class variables
|
||||
for f_path, f_data in mapper.files.items():
|
||||
if f_path not in report: continue
|
||||
for c_name, c_data in f_data["classes"].items():
|
||||
if c_name not in report[f_path]["classes"]:
|
||||
report[f_path]["classes"][c_name] = {"methods": {}, "variables": {}}
|
||||
|
||||
class_vars_summary = []
|
||||
for v_name, v_data in c_data["variables"].items():
|
||||
# EXTERNAL FILTER: Only include mutations/usages from different files
|
||||
ext_muts = [f"{m['file']}:{m['line']}, {m['method']}" for m in v_data["mutations"] if m['file'] != f_path]
|
||||
ext_usages = [u for u in v_data["usages"] if u != f_path]
|
||||
|
||||
if not ext_muts and not ext_usages:
|
||||
continue
|
||||
|
||||
m_tag = f"[M: {'; '.join(ext_muts or ['None'])}]"
|
||||
u_tag = f"[U: {', '.join(sorted(list(ext_usages or ['None'])))}]"
|
||||
tag = f"{m_tag} {u_tag}"
|
||||
report[f_path]["classes"][c_name]["variables"][v_name] = tag
|
||||
class_vars_summary.append(f"{v_name}: {tag}")
|
||||
|
||||
if class_vars_summary:
|
||||
report[f_path]["classes"][c_name]["class_tag"] = "\n".join(class_vars_summary)
|
||||
|
||||
print(json.dumps(report, indent=1))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+62
-32
@@ -61,9 +61,10 @@ def resolve_paths(base_dir: Path, entry: str) -> list[Path]:
|
||||
|
||||
def build_discussion_section(history: list[Any]) -> str:
|
||||
"""
|
||||
Builds a markdown section for discussion history.
|
||||
Handles both legacy list[str] and new list[dict].
|
||||
"""
|
||||
|
||||
Builds a markdown section for discussion history.
|
||||
Handles both legacy list[str] and new list[dict].
|
||||
"""
|
||||
sections = []
|
||||
for i, entry in enumerate(history, start=1):
|
||||
if isinstance(entry, dict):
|
||||
@@ -76,6 +77,9 @@ def build_discussion_section(history: list[Any]) -> str:
|
||||
return "\n\n---\n\n".join(sections)
|
||||
|
||||
def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str:
|
||||
"""
|
||||
[C: tests/test_tiered_context.py:test_build_files_section_with_dicts]
|
||||
"""
|
||||
sections = []
|
||||
for entry_raw in files:
|
||||
if isinstance(entry_raw, dict):
|
||||
@@ -120,19 +124,21 @@ def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str:
|
||||
|
||||
def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Return a list of dicts describing each file, for use by ai_client when it
|
||||
wants to upload individual files rather than inline everything as markdown.
|
||||
|
||||
Each dict has:
|
||||
path : Path (resolved absolute path)
|
||||
entry : str (original config entry string)
|
||||
content : str (file text, or error string)
|
||||
error : bool
|
||||
mtime : float (last modification time, for skip-if-unchanged optimization)
|
||||
tier : int | None (optional tier for context management)
|
||||
auto_aggregate : bool
|
||||
force_full : bool
|
||||
"""
|
||||
|
||||
Return a list of dicts describing each file, for use by ai_client when it
|
||||
wants to upload individual files rather than inline everything as markdown.
|
||||
|
||||
Each dict has:
|
||||
path : Path (resolved absolute path)
|
||||
entry : str (original config entry string)
|
||||
content : str (file text, or error string)
|
||||
error : bool
|
||||
mtime : float (last modification time, for skip-if-unchanged optimization)
|
||||
tier : int | None (optional tier for context management)
|
||||
auto_aggregate : bool
|
||||
force_full : bool
|
||||
[C: src/app_controller.py:AppController._bg_task, src/orchestrator_pm.py:module, tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_tiered_context.py:test_build_file_items_with_tiers]
|
||||
"""
|
||||
with get_monitor().scope("build_file_items"):
|
||||
items: list[dict[str, Any]] = []
|
||||
for entry_raw in files:
|
||||
@@ -175,15 +181,19 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
|
||||
|
||||
def build_summary_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build a compact summary section using summarize.py — one short block per file.
|
||||
Used as the initial <context> block instead of full file contents.
|
||||
"""
|
||||
|
||||
Build a compact summary section using summarize.py — one short block per file.
|
||||
Used as the initial <context> block instead of full file contents.
|
||||
"""
|
||||
with get_monitor().scope("build_summary_section"):
|
||||
items = build_file_items(base_dir, files)
|
||||
return summarize.build_summary_markdown(items)
|
||||
|
||||
def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
|
||||
"""Build the files markdown section from pre-read file items (avoids double I/O)."""
|
||||
"""
|
||||
Build the files markdown section from pre-read file items (avoids double I/O).
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_ui_summary_only_removal.py:test_aggregate_from_items_respects_auto_aggregate]
|
||||
"""
|
||||
sections = []
|
||||
for item in file_items:
|
||||
if not item.get("auto_aggregate", True):
|
||||
@@ -201,6 +211,9 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
|
||||
return "\n\n---\n\n".join(sections)
|
||||
|
||||
def build_beads_section(base_dir: Path) -> str:
|
||||
"""
|
||||
[C: tests/test_aggregate_beads.py:test_build_beads_compaction]
|
||||
"""
|
||||
client = beads_client.BeadsClient(base_dir)
|
||||
if not client.is_initialized():
|
||||
return ""
|
||||
@@ -247,20 +260,28 @@ def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str:
|
||||
"""Build markdown with only files + screenshots (no history). Used for stable caching."""
|
||||
"""
|
||||
Build markdown with only files + screenshots (no history). Used for stable caching.
|
||||
[C: src/app_controller.py:AppController._do_generate, tests/test_history_management.py:test_aggregate_blacklist]
|
||||
"""
|
||||
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only, aggregation_strategy=aggregation_strategy)
|
||||
|
||||
def build_discussion_text(history: list[str]) -> str:
|
||||
"""Build just the discussion history section text. Returns empty string if no history."""
|
||||
"""
|
||||
Build just the discussion history section text. Returns empty string if no history.
|
||||
[C: src/app_controller.py:AppController._do_generate, tests/test_history_management.py:test_aggregate_includes_segregated_history]
|
||||
"""
|
||||
if not history:
|
||||
return ""
|
||||
return "## Discussion History\n\n" + build_discussion_section(history)
|
||||
|
||||
def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str:
|
||||
"""
|
||||
Tier 1 Context: Strategic/Orchestration.
|
||||
Full content for core conductor files and files with tier=1, summaries for others.
|
||||
"""
|
||||
|
||||
Tier 1 Context: Strategic/Orchestration.
|
||||
Full content for core conductor files and files with tier=1, summaries for others.
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_tiered_context.py:test_build_tier1_context_exists, tests/test_tiered_context.py:test_tiered_context_by_tier_field]
|
||||
"""
|
||||
core_files = {"product.md", "tech-stack.md", "workflow.md", "tracks.md"}
|
||||
sections = []
|
||||
for item in file_items:
|
||||
@@ -289,16 +310,20 @@ def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
|
||||
def build_tier2_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str:
|
||||
"""
|
||||
Tier 2 Context: Architectural/Tech Lead.
|
||||
Full content for all files (standard behavior).
|
||||
"""
|
||||
|
||||
Tier 2 Context: Architectural/Tech Lead.
|
||||
Full content for all files (standard behavior).
|
||||
[C: tests/test_tiered_context.py:test_build_tier2_context_exists]
|
||||
"""
|
||||
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, summary_only=False)
|
||||
|
||||
def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], focus_files: list[str]) -> str:
|
||||
"""
|
||||
Tier 3 Context: Execution/Worker.
|
||||
Full content for focus_files and files with tier=3, summaries/skeletons for others.
|
||||
"""
|
||||
|
||||
Tier 3 Context: Execution/Worker.
|
||||
Full content for focus_files and files with tier=3, summaries/skeletons for others.
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_tiered_context.py:test_build_tier3_context_ast_skeleton, tests/test_tiered_context.py:test_build_tier3_context_exists, tests/test_tiered_context.py:test_tiered_context_by_tier_field]
|
||||
"""
|
||||
with get_monitor().scope("build_tier3_context"):
|
||||
focus_set = set(focus_files)
|
||||
parser = ASTParser("python")
|
||||
@@ -362,6 +387,9 @@ def build_markdown(base_dir: Path, files: list[str | dict[str, Any]], screenshot
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str, Path, list[dict[str, Any]]]:
|
||||
"""
|
||||
[C: simulation/sim_base.py:run_sim, src/ai_client.py:_send_anthropic, src/ai_client.py:_send_deepseek, src/ai_client.py:_send_gemini, src/ai_client.py:_send_gemini_cli, src/ai_client.py:_send_minimax, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._start_track_logic, src/external_editor.py:_find_vscode_in_registry, src/gui_2.py:App._render_snapshot_tab, src/gui_2.py:App.run, src/gui_2.py:main, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
namespace = config.get("project", {}).get("name")
|
||||
if not namespace:
|
||||
namespace = config.get("output", {}).get("namespace", "project")
|
||||
@@ -385,6 +413,9 @@ def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str
|
||||
|
||||
def main() -> None:
|
||||
# Load global config to find active project
|
||||
"""
|
||||
[C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/api_hooks.py:WebSocketServer._run_loop, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_post_process.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module]
|
||||
"""
|
||||
from src.paths import get_config_path
|
||||
config_path = get_config_path()
|
||||
if not config_path.exists():
|
||||
@@ -406,4 +437,3 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
+278
-27
@@ -42,6 +42,9 @@ from src import rag_engine
|
||||
from src import theme_2 as theme
|
||||
|
||||
def hide_tk_root() -> Tk:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._render_screenshots_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App.render_path_field]
|
||||
"""
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
root.wm_attributes("-topmost", True)
|
||||
@@ -49,12 +52,17 @@ def hide_tk_root() -> Tk:
|
||||
|
||||
def parse_symbols(text: str) -> list[str]:
|
||||
"""
|
||||
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
|
||||
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
|
||||
|
||||
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
|
||||
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
|
||||
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_basic, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_edge_cases, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_methods, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_mixed, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_no_symbols]
|
||||
"""
|
||||
return re.findall(r"@([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)", text)
|
||||
|
||||
def get_symbol_definition(symbol: str, files: list[str]) -> tuple[str, str, int] | None:
|
||||
"""
|
||||
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_found, tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_not_found]
|
||||
"""
|
||||
for file_path in files:
|
||||
result = mcp_client.py_get_symbol_info(file_path, symbol)
|
||||
if isinstance(result, tuple):
|
||||
@@ -74,6 +82,9 @@ class ConfirmRequest(BaseModel):
|
||||
|
||||
class ConfirmDialog:
|
||||
def __init__(self, script: str, base_dir: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._uid = str(uuid.uuid4())
|
||||
self._script = str(script) if script is not None else ""
|
||||
self._base_dir = str(base_dir) if base_dir is not None else ""
|
||||
@@ -82,6 +93,9 @@ class ConfirmDialog:
|
||||
self._approved = False
|
||||
|
||||
def wait(self) -> tuple[bool, str]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
while not self._done:
|
||||
@@ -92,12 +106,18 @@ class ConfirmDialog:
|
||||
|
||||
class MMAApprovalDialog:
|
||||
def __init__(self, ticket_id: str, payload: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._payload = payload
|
||||
self._condition = threading.Condition()
|
||||
self._done = False
|
||||
self._approved = False
|
||||
|
||||
def wait(self) -> tuple[bool, str]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
while not self._done:
|
||||
@@ -108,6 +128,9 @@ class MMAApprovalDialog:
|
||||
|
||||
class MMASpawnApprovalDialog:
|
||||
def __init__(self, ticket_id: str, role: str, prompt: str, context_md: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._prompt = prompt
|
||||
self._context_md = context_md
|
||||
self._condition = threading.Condition()
|
||||
@@ -116,6 +139,9 @@ class MMASpawnApprovalDialog:
|
||||
self._abort = False
|
||||
|
||||
def wait(self) -> dict[str, Any]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
while not self._done:
|
||||
@@ -131,12 +157,16 @@ class MMASpawnApprovalDialog:
|
||||
|
||||
class AppController:
|
||||
"""
|
||||
The headless controller for the Manual Slop application.
|
||||
Owns the application state and manages background services.
|
||||
|
||||
The headless controller for the Manual Slop application.
|
||||
Owns the application state and manages background services.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize locks first to avoid initialization order issues
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._send_thread_lock: threading.Lock = threading.Lock()
|
||||
self._disc_entries_lock: threading.Lock = threading.Lock()
|
||||
self._pending_comms_lock: threading.Lock = threading.Lock()
|
||||
@@ -486,7 +516,10 @@ class AppController:
|
||||
return self.ui_files_base_dir
|
||||
|
||||
def _update_inject_preview(self) -> None:
|
||||
"""Updates the preview content based on the selected file and injection mode."""
|
||||
"""
|
||||
Updates the preview content based on the selected file and injection mode.
|
||||
[C: src/gui_2.py:App._gui_func, tests/test_skeleton_injection.py:test_update_inject_preview_full, tests/test_skeleton_injection.py:test_update_inject_preview_skeleton, tests/test_skeleton_injection.py:test_update_inject_preview_truncation]
|
||||
"""
|
||||
if not self._inject_file_path:
|
||||
self._inject_preview = ""
|
||||
return
|
||||
@@ -720,6 +753,9 @@ class AppController:
|
||||
|
||||
def _process_pending_gui_tasks(self) -> None:
|
||||
# Periodic telemetry broadcast
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, tests/test_api_hook_extensions.py:test_app_processes_new_actions, tests/test_gui_updates.py:test_gui_updates_on_event, tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_mma_orchestration_gui.py:test_handle_ai_response_fallback, tests/test_mma_orchestration_gui.py:test_handle_ai_response_with_stream_id, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_mma_spawn_approval, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_show_track_proposal, tests/test_process_pending_gui_tasks.py:test_gcli_path_updates_adapter, tests/test_process_pending_gui_tasks.py:test_redundant_calls_in_process_pending_gui_tasks]
|
||||
"""
|
||||
now = time.time()
|
||||
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server:
|
||||
if now - self._last_telemetry_time >= 1.0:
|
||||
@@ -918,6 +954,9 @@ class AppController:
|
||||
class AutoSpawnDialog:
|
||||
def __init__(self, t): self.t = t
|
||||
def wait(self):
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
return {'approved': True, 'abort': False, 'prompt': self.t.get("prompt"), 'context_md': self.t.get("context_md")}
|
||||
task["dialog_container"][0] = AutoSpawnDialog(task)
|
||||
continue
|
||||
@@ -987,7 +1026,10 @@ class AppController:
|
||||
print(f"Error executing GUI task: {e}")
|
||||
|
||||
def _process_pending_history_adds(self) -> None:
|
||||
"""Synchronizes pending history entries to the active discussion and project state."""
|
||||
"""
|
||||
Synchronizes pending history entries to the active discussion and project state.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
with self._pending_history_adds_lock:
|
||||
items = self._pending_history_adds[:]
|
||||
self._pending_history_adds.clear()
|
||||
@@ -1012,7 +1054,10 @@ class AppController:
|
||||
self.disc_entries.append(item)
|
||||
|
||||
def _process_pending_tool_calls(self) -> bool:
|
||||
"""Drains pending tool calls into the tool log. Returns True if any were processed."""
|
||||
"""
|
||||
Drains pending tool calls into the tool log. Returns True if any were processed.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
with self._pending_tool_calls_lock:
|
||||
items = self._pending_tool_calls[:]
|
||||
self._pending_tool_calls.clear()
|
||||
@@ -1056,7 +1101,10 @@ class AppController:
|
||||
self._pending_dialog = None
|
||||
|
||||
def init_state(self):
|
||||
"""Initializes the application state from configurations."""
|
||||
"""
|
||||
Initializes the application state from configurations.
|
||||
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._render_paths_panel, src/gui_2.py:App._save_paths, tests/test_app_controller_mcp.py:test_app_controller_mcp_loading, tests/test_app_controller_mcp.py:test_app_controller_mcp_project_override, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_init_state_loads_prompts]
|
||||
"""
|
||||
self.active_tickets = []
|
||||
self.ui_separate_task_dag = False
|
||||
self.ui_separate_usage_analytics = False
|
||||
@@ -1198,6 +1246,9 @@ class AppController:
|
||||
self.event_queue.put('refresh_external_mcps', None)
|
||||
|
||||
async def refresh_external_mcps(self):
|
||||
"""
|
||||
[C: tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
await mcp_client.get_external_mcp_manager().stop_all()
|
||||
# Start servers with auto_start=True
|
||||
for name, cfg in self.mcp_config.mcpServers.items():
|
||||
@@ -1205,6 +1256,9 @@ class AppController:
|
||||
await mcp_client.get_external_mcp_manager().add_server(cfg)
|
||||
|
||||
def cb_load_prior_log(self, path: Optional[str] = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_log_management]
|
||||
"""
|
||||
root = hide_tk_root()
|
||||
if path is None:
|
||||
path = filedialog.askdirectory(
|
||||
@@ -1393,6 +1447,9 @@ class AppController:
|
||||
|
||||
|
||||
def cb_exit_prior_session(self):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
self.is_viewing_prior_session = False
|
||||
if self._current_session_usage:
|
||||
self.session_usage = self._current_session_usage
|
||||
@@ -1475,6 +1532,9 @@ class AppController:
|
||||
thread.start()
|
||||
|
||||
def _fetch_models(self, provider: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App.run]
|
||||
"""
|
||||
self.ai_status = "fetching models..."
|
||||
|
||||
def do_fetch() -> None:
|
||||
@@ -1498,14 +1558,20 @@ class AppController:
|
||||
self.models_thread.start()
|
||||
|
||||
def start_services(self, app: Any = None):
|
||||
"""Starts background threads."""
|
||||
"""
|
||||
Starts background threads.
|
||||
[C: src/gui_2.py:App.__init__]
|
||||
"""
|
||||
self._prune_old_logs()
|
||||
self._init_ai_and_hooks(app)
|
||||
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self._loop_thread.start()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Stops background threads and cleans up resources."""
|
||||
"""
|
||||
Stops background threads and cleans up resources.
|
||||
[C: src/gui_2.py:App.run, src/gui_2.py:App.shutdown, tests/conftest.py:app_instance, tests/conftest.py:mock_app]
|
||||
"""
|
||||
from src import ai_client
|
||||
ai_client.cleanup()
|
||||
if hasattr(self, 'hook_server') and self.hook_server:
|
||||
@@ -1604,7 +1670,10 @@ class AppController:
|
||||
asyncio.run(self.refresh_external_mcps())
|
||||
|
||||
def _handle_request_event(self, event: events.UserRequestEvent) -> None:
|
||||
"""Processes a UserRequestEvent by calling the AI client."""
|
||||
"""
|
||||
Processes a UserRequestEvent by calling the AI client.
|
||||
[C: tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_rag_integration.py:test_rag_integration]
|
||||
"""
|
||||
self.ai_status = 'sending...'
|
||||
ai_client.set_current_tier(None) # Ensure main discussion is untagged
|
||||
# Clear response area for new turn
|
||||
@@ -1662,6 +1731,9 @@ class AppController:
|
||||
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
|
||||
|
||||
def _on_comms_entry(self, entry: Dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: tests/test_app_controller_offloading.py:test_on_comms_entry_tool_result_offloading]
|
||||
"""
|
||||
optimized_entry = self._offload_entry_payload(entry)
|
||||
session_logger.log_comms(optimized_entry)
|
||||
entry["local_ts"] = time.time()
|
||||
@@ -1756,6 +1828,9 @@ class AppController:
|
||||
self._pending_comms.append(entry)
|
||||
|
||||
def _on_tool_log(self, script: str, result: str) -> None:
|
||||
"""
|
||||
[C: tests/test_app_controller_offloading.py:test_on_tool_log_offloading]
|
||||
"""
|
||||
session_logger.log_tool_call(script, result, None)
|
||||
session_logger.log_tool_output(result)
|
||||
source_tier = ai_client.get_current_tier()
|
||||
@@ -1763,6 +1838,9 @@ class AppController:
|
||||
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
|
||||
|
||||
def _on_api_event(self, event_name: str = "generic_event", **kwargs: Any) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_updates.py:test_gui_updates_on_event]
|
||||
"""
|
||||
payload = kwargs.get("payload", {})
|
||||
# Push to background event queue, NOT GUI queue
|
||||
self.event_queue.put("refresh_api_metrics", payload)
|
||||
@@ -1778,6 +1856,9 @@ class AppController:
|
||||
})
|
||||
|
||||
def _confirm_and_run(self, script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Optional[str]:
|
||||
"""
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mutating_tool_triggers_callback, tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_rejection_prevents_dispatch]
|
||||
"""
|
||||
if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False):
|
||||
self.ai_status = "running powershell..."
|
||||
output = shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
|
||||
@@ -1815,6 +1896,9 @@ class AppController:
|
||||
return output
|
||||
|
||||
def _append_tool_log(self, script: str, result: str, source_tier: str | None = None, elapsed_ms: float = 0.0) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_has_source_tier, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_keys, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_stores_dict]
|
||||
"""
|
||||
self._tool_log.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
|
||||
tool_name = self._extract_tool_name(script)
|
||||
is_failure = "REJECTED" in result or "Error" in result or "error" in result.lower()
|
||||
@@ -1910,7 +1994,10 @@ class AppController:
|
||||
self._token_stats_dirty = True
|
||||
|
||||
def create_api(self) -> FastAPI:
|
||||
"""Creates and configures the FastAPI application for headless mode."""
|
||||
"""
|
||||
Creates and configures the FastAPI application for headless mode.
|
||||
[C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp]
|
||||
"""
|
||||
api = FastAPI(title="Manual Slop Headless API")
|
||||
API_KEY_NAME = "X-API-KEY"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
@@ -1934,7 +2021,10 @@ class AppController:
|
||||
|
||||
@api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
|
||||
def get_gui_state() -> dict[str, Any]:
|
||||
"""Returns the current GUI state for specific fields."""
|
||||
"""
|
||||
Returns the current GUI state for specific fields.
|
||||
[C: tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_conductor_api_hook_integration.py:simulate_conductor_phase_completion, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_workflow.py:test_full_live_workflow, tests/test_live_workflow.py:wait_for_value, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tool_management_layout.py:test_tool_management_gettable_fields, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout]
|
||||
"""
|
||||
gettable = getattr(self, "_gettable_fields", {})
|
||||
state = {}
|
||||
import dataclasses
|
||||
@@ -1948,7 +2038,10 @@ class AppController:
|
||||
|
||||
@api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)])
|
||||
def get_mma_status() -> dict[str, Any]:
|
||||
"""Dedicated endpoint for MMA-related status."""
|
||||
"""
|
||||
Dedicated endpoint for MMA-related status.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:_poll_mma_status, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:_poll_mma_status, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:_poll]
|
||||
"""
|
||||
return {
|
||||
"mma_status": self.mma_status,
|
||||
"ai_status": self.ai_status,
|
||||
@@ -1963,7 +2056,10 @@ class AppController:
|
||||
|
||||
@api.post("/api/gui", dependencies=[Depends(get_api_key)])
|
||||
def post_gui(req: dict) -> dict[str, str]:
|
||||
"""Pushes a GUI task to the event queue."""
|
||||
"""
|
||||
Pushes a GUI task to the event queue.
|
||||
[C: tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_api_hook_client.py:test_post_gui_success, tests/test_gui2_parity.py:test_gui2_custom_callback_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_visual_mma.py:test_visual_mma_components]
|
||||
"""
|
||||
self.event_queue.put("gui_task", req)
|
||||
return {"status": "queued"}
|
||||
|
||||
@@ -1988,7 +2084,10 @@ class AppController:
|
||||
|
||||
@api.get("/api/performance", dependencies=[Depends(get_api_key)])
|
||||
def get_performance() -> dict[str, Any]:
|
||||
"""Returns performance monitor metrics."""
|
||||
"""
|
||||
Returns performance monitor metrics.
|
||||
[C: tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_gui_performance_requirements.py:test_idle_performance_requirements, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing]
|
||||
"""
|
||||
return {"performance": self.perf_monitor.get_metrics()}
|
||||
|
||||
@api.get("/api/gui/diagnostics", dependencies=[Depends(get_api_key)])
|
||||
@@ -2107,7 +2206,10 @@ class AppController:
|
||||
|
||||
@api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
|
||||
def get_session(session_id: str) -> dict[str, Any]:
|
||||
"""Returns the content of the comms.log for a specific session."""
|
||||
"""
|
||||
Returns the content of the comms.log for a specific session.
|
||||
[C: simulation/ping_pong.py:main, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/sim_tools.py:ToolsSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/test_api_hook_client.py:test_get_session_success, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_live_workflow.py:test_full_live_workflow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim]
|
||||
"""
|
||||
log_path = paths.get_logs_dir() / session_id / "comms.log"
|
||||
if not log_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Session log not found")
|
||||
@@ -2162,15 +2264,24 @@ class AppController:
|
||||
self.ai_status = "config saved"
|
||||
|
||||
def _cb_reset_base_prompt(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
self.ui_base_system_prompt = ai_client._SYSTEM_PROMPT
|
||||
self.ui_use_default_base_prompt = False
|
||||
|
||||
def _cb_clear_summary_cache(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
"""
|
||||
from src import summarize
|
||||
summarize._summary_cache.clear()
|
||||
self._push_mma_state_update()
|
||||
|
||||
def _cb_show_base_prompt_diff(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
self._show_base_prompt_diff_modal = True
|
||||
|
||||
def _cb_disc_create(self) -> None:
|
||||
@@ -2180,6 +2291,9 @@ class AppController:
|
||||
self.ui_disc_new_name_input = ""
|
||||
|
||||
def _switch_project(self, path: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_projects_panel]
|
||||
"""
|
||||
if not Path(path).exists():
|
||||
self.ai_status = f"project file not found: {path}"
|
||||
return
|
||||
@@ -2200,6 +2314,9 @@ class AppController:
|
||||
|
||||
def _refresh_from_project(self) -> None:
|
||||
# Deserialize FileItems in files.paths
|
||||
"""
|
||||
[C: tests/test_mma_dashboard_refresh.py:test_mma_dashboard_initialization_refresh, tests/test_mma_dashboard_refresh.py:test_mma_dashboard_refresh]
|
||||
"""
|
||||
raw_paths = self.project.get("files", {}).get("paths", [])
|
||||
self.files = []
|
||||
for p in raw_paths:
|
||||
@@ -2279,6 +2396,9 @@ class AppController:
|
||||
self._rebuild_rag_index()
|
||||
|
||||
def _cb_save_workspace_profile(self, name: str, scope: str = 'project') -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_save_workspace_profile_modal]
|
||||
"""
|
||||
if not hasattr(self, '_app') or not self._app:
|
||||
return
|
||||
profile = self._app._capture_workspace_profile(name)
|
||||
@@ -2287,18 +2407,27 @@ class AppController:
|
||||
self._app.workspace_profiles = self.workspace_profiles
|
||||
|
||||
def _cb_delete_workspace_profile(self, name: str, scope: str = 'project') -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
self.workspace_manager.delete_profile(name, scope=scope)
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
if hasattr(self, '_app') and self._app:
|
||||
self._app.workspace_profiles = self.workspace_profiles
|
||||
|
||||
def _cb_load_workspace_profile(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
if name in self.workspace_profiles:
|
||||
profile = self.workspace_profiles[name]
|
||||
if hasattr(self, '_app') and self._app:
|
||||
self._app._apply_workspace_profile(profile)
|
||||
|
||||
def _apply_preset(self, name: str, scope: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
print(f"[DEBUG] _apply_preset: name={name}, scope={scope}")
|
||||
if name == "None":
|
||||
if scope == "global":
|
||||
@@ -2318,6 +2447,9 @@ class AppController:
|
||||
self.ui_project_preset_name = name
|
||||
|
||||
def _cb_save_preset(self, name, content, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
"""
|
||||
print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}")
|
||||
if not name or not name.strip():
|
||||
raise ValueError("Preset name cannot be empty or whitespace.")
|
||||
@@ -2330,19 +2462,31 @@ class AppController:
|
||||
print(f"[DEBUG] _cb_save_preset: saved {name}, total presets now {len(self.presets)}")
|
||||
|
||||
def _cb_delete_preset(self, name, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
"""
|
||||
self.preset_manager.delete_preset(name, scope)
|
||||
self.presets = self.preset_manager.load_all()
|
||||
|
||||
def _cb_save_tool_preset(self, name, categories, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
preset = models.ToolPreset(name=name, categories=categories)
|
||||
self.tool_preset_manager.save_preset(preset, scope)
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
|
||||
def _cb_delete_tool_preset(self, name, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
self.tool_preset_manager.delete_preset(name, scope)
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
|
||||
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
self.tool_preset_manager.save_bias_profile(profile, scope)
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
|
||||
@@ -2351,15 +2495,24 @@ class AppController:
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
|
||||
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
"""
|
||||
self.persona_manager.save_persona(persona, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
def _cb_delete_persona(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
"""
|
||||
self.persona_manager.delete_persona(name, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
|
||||
def _cb_load_track(self, track_id: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard]
|
||||
"""
|
||||
state = project_manager.load_track_state(track_id, self.active_project_root)
|
||||
if state:
|
||||
try:
|
||||
@@ -2391,6 +2544,9 @@ class AppController:
|
||||
print(f"Error loading track {track_id}: {e}")
|
||||
|
||||
def _save_active_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App.delete_context_preset, src/gui_2.py:App.save_context_preset]
|
||||
"""
|
||||
if self.active_project_path:
|
||||
try:
|
||||
cleaned = project_manager.clean_nones(self.project)
|
||||
@@ -2399,11 +2555,17 @@ class AppController:
|
||||
self.ai_status = f"save error: {e}"
|
||||
|
||||
def _get_discussion_names(self) -> list[str]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
return sorted(discussions.keys())
|
||||
|
||||
def _switch_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_takes_panel]
|
||||
"""
|
||||
self._flush_disc_entries_to_project()
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
@@ -2419,6 +2581,9 @@ class AppController:
|
||||
self.ai_status = f"discussion: {name}"
|
||||
|
||||
def _flush_disc_entries_to_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
|
||||
if self.active_track and self._track_discussion_active:
|
||||
project_manager.save_track_history(self.active_track.id, history_strings, self.active_project_root)
|
||||
@@ -2430,6 +2595,9 @@ class AppController:
|
||||
disc_data["last_updated"] = project_manager.now_ts()
|
||||
|
||||
def _create_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel]
|
||||
"""
|
||||
disc_sec = self.project.setdefault("discussion", {})
|
||||
discussions = disc_sec.setdefault("discussions", {})
|
||||
if name in discussions:
|
||||
@@ -2439,6 +2607,9 @@ class AppController:
|
||||
self._switch_discussion(name)
|
||||
|
||||
def _branch_discussion(self, index: int) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
self._flush_disc_entries_to_project()
|
||||
# Generate a unique branch name
|
||||
base_name = self.active_discussion.split("_take_")[0]
|
||||
@@ -2453,6 +2624,9 @@ class AppController:
|
||||
project_manager.branch_discussion(self.project, self.active_discussion, new_name, index)
|
||||
self._switch_discussion(new_name)
|
||||
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
if old_name not in discussions:
|
||||
@@ -2466,6 +2640,9 @@ class AppController:
|
||||
disc_sec["active"] = new_name
|
||||
|
||||
def _delete_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
if len(discussions) <= 1:
|
||||
@@ -2479,6 +2656,9 @@ class AppController:
|
||||
self._switch_discussion(remaining[0])
|
||||
|
||||
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._handle_approve_mma_step, src/gui_2.py:App._handle_approve_spawn]
|
||||
"""
|
||||
if self._pending_mma_approvals:
|
||||
task = self._pending_mma_approvals.pop(0)
|
||||
dlg = task.get("dialog_container", [None])[0]
|
||||
@@ -2504,7 +2684,10 @@ class AppController:
|
||||
spawn_dlg._condition.notify_all()
|
||||
|
||||
def _handle_approve_ask(self) -> None:
|
||||
"""Responds with approval for a pending /api/ask request."""
|
||||
"""
|
||||
Responds with approval for a pending /api/ask request.
|
||||
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
if not self._ask_request_id: return
|
||||
request_id = self._ask_request_id
|
||||
|
||||
@@ -2522,7 +2705,10 @@ class AppController:
|
||||
self._ask_tool_data = None
|
||||
|
||||
def _handle_reject_ask(self) -> None:
|
||||
"""Responds with rejection for a pending /api/ask request."""
|
||||
"""
|
||||
Responds with rejection for a pending /api/ask request.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
if not self._ask_request_id: return
|
||||
request_id = self._ask_request_id
|
||||
|
||||
@@ -2540,7 +2726,10 @@ class AppController:
|
||||
self._ask_tool_data = None
|
||||
|
||||
def _handle_reset_session(self) -> None:
|
||||
"""Logic for resetting the AI session and GUI state."""
|
||||
"""
|
||||
Logic for resetting the AI session and GUI state.
|
||||
[C: src/gui_2.py:App._render_message_panel]
|
||||
"""
|
||||
ai_client.reset_session()
|
||||
ai_client.clear_comms_log()
|
||||
self._tool_log.clear()
|
||||
@@ -2568,9 +2757,15 @@ class AppController:
|
||||
self._pending_gui_tasks.clear()
|
||||
|
||||
def _handle_md_only(self) -> None:
|
||||
"""Logic for the 'MD Only' action."""
|
||||
"""
|
||||
Logic for the 'MD Only' action.
|
||||
[C: src/gui_2.py:App._render_message_panel]
|
||||
"""
|
||||
|
||||
def worker():
|
||||
"""
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
try:
|
||||
md, path, *_ = self._do_generate()
|
||||
self.last_md = md
|
||||
@@ -2583,9 +2778,15 @@ class AppController:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _handle_generate_send(self) -> None:
|
||||
"""Logic for the 'Gen + Send' action."""
|
||||
"""
|
||||
Logic for the 'Gen + Send' action.
|
||||
[C: src/gui_2.py:App._render_message_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel, tests/test_gui_events_v2.py:test_handle_generate_send_pushes_event, tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
|
||||
def worker():
|
||||
"""
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
sys.stderr.write("[DEBUG] _handle_generate_send worker started\n")
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
@@ -2650,6 +2851,9 @@ class AppController:
|
||||
self._cached_files = stats.get("cached_files", [])
|
||||
|
||||
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_updates.py:test_telemetry_data_updates_correctly]
|
||||
"""
|
||||
if "latency" in payload:
|
||||
self.session_usage["last_latency"] = payload["latency"]
|
||||
if "usage" in payload and "percentage" in payload["usage"]:
|
||||
@@ -2674,11 +2878,17 @@ class AppController:
|
||||
self._cached_tool_stats = dict(self._tool_stats)
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_cache_panel]
|
||||
"""
|
||||
from src import ai_client
|
||||
ai_client.cleanup()
|
||||
self._update_cached_stats()
|
||||
|
||||
def get_session_insights(self) -> Dict[str, Any]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_session_insights_panel]
|
||||
"""
|
||||
from src import cost_tracker
|
||||
total_input = sum(e["input"] for e in self._token_history)
|
||||
total_output = sum(e["output"] for e in self._token_history)
|
||||
@@ -2701,6 +2911,9 @@ class AppController:
|
||||
}
|
||||
|
||||
def _flush_to_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
proj = self.project
|
||||
proj.setdefault("output", {})["output_dir"] = self.ui_output_dir
|
||||
proj.setdefault("files", {})["base_dir"] = self.ui_files_base_dir
|
||||
@@ -2738,6 +2951,9 @@ class AppController:
|
||||
project_manager.save_project(cleaned_proj, self.active_project_path)
|
||||
|
||||
def _flush_to_config(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App._show_menus, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_flush_saves_prompts]
|
||||
"""
|
||||
self.config["ai"] = {
|
||||
"provider": self.current_provider,
|
||||
"model": self.current_model,
|
||||
@@ -2778,7 +2994,10 @@ class AppController:
|
||||
theme.save_to_config(self.config)
|
||||
|
||||
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).
|
||||
[C: src/gui_2.py:App._show_menus, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy]
|
||||
"""
|
||||
self._flush_to_project()
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
@@ -2809,6 +3028,9 @@ class AppController:
|
||||
return full_md, path, file_items, stable_md, discussion_text
|
||||
|
||||
def _cb_plan_epic(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread]
|
||||
"""
|
||||
def _bg_task() -> None:
|
||||
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
|
||||
sys.stderr.flush()
|
||||
@@ -2855,6 +3077,9 @@ class AppController:
|
||||
threading.Thread(target=_bg_task, daemon=True).start()
|
||||
|
||||
def _cb_accept_tracks(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
"""
|
||||
self._show_track_proposal_modal = False
|
||||
|
||||
def _bg_task() -> None:
|
||||
@@ -2896,6 +3121,9 @@ class AppController:
|
||||
threading.Thread(target=_bg_task, daemon=True).start()
|
||||
|
||||
def _cb_start_track(self, user_data: Any = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
"""
|
||||
if isinstance(user_data, str):
|
||||
# If track_id is provided directly
|
||||
track_id = user_data
|
||||
@@ -3005,6 +3233,9 @@ class AppController:
|
||||
print(f"ERROR in _start_track_logic: {e}")
|
||||
|
||||
def _cb_ticket_retry(self, ticket_id: str) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_retry]
|
||||
"""
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'todo'
|
||||
@@ -3012,6 +3243,9 @@ class AppController:
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
|
||||
|
||||
def _cb_ticket_skip(self, ticket_id: str) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_skip]
|
||||
"""
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'skipped'
|
||||
@@ -3031,7 +3265,10 @@ class AppController:
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
|
||||
|
||||
def kill_worker(self, worker_id: str) -> None:
|
||||
"""Aborts a running worker."""
|
||||
"""
|
||||
Aborts a running worker.
|
||||
[C: src/gui_2.py:App._cb_kill_ticket, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread]
|
||||
"""
|
||||
engine = self.engines.get(self.active_track.id if self.active_track else None)
|
||||
if engine:
|
||||
engine.kill_worker(worker_id)
|
||||
@@ -3051,7 +3288,10 @@ class AppController:
|
||||
engine.resume()
|
||||
|
||||
def inject_context(self, data: dict) -> None:
|
||||
"""Programmatic context injection."""
|
||||
"""
|
||||
Programmatic context injection.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
file_path = data.get("file_path")
|
||||
if file_path:
|
||||
if not os.path.isabs(file_path):
|
||||
@@ -3097,6 +3337,9 @@ class AppController:
|
||||
self._push_mma_state_update()
|
||||
|
||||
def _cb_run_conductor_setup(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_conductor_setup_scan]
|
||||
"""
|
||||
base = paths.get_conductor_dir(project_path=self.active_project_root)
|
||||
if not base.exists():
|
||||
self.ui_conductor_setup_summary = f"Error: {base}/ directory not found."
|
||||
@@ -3124,6 +3367,9 @@ class AppController:
|
||||
self.ui_conductor_setup_summary = "\n".join(summary)
|
||||
|
||||
def _cb_create_track(self, name: str, desc: str, track_type: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_create_track]
|
||||
"""
|
||||
if not name: return
|
||||
date_suffix = datetime.now().strftime("%Y%m%d")
|
||||
track_id = f"{name.lower().replace(' ', '_')}_{date_suffix}"
|
||||
@@ -3149,6 +3395,9 @@ class AppController:
|
||||
self.tracks = project_manager.get_all_tracks(self.active_project_root)
|
||||
|
||||
def _push_mma_state_update(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._cb_block_ticket, src/gui_2.py:App._cb_unblock_ticket, src/gui_2.py:App._render_mma_dashboard, src/gui_2.py:App._render_task_dag_panel, src/gui_2.py:App._render_ticket_queue, src/gui_2.py:App._reorder_ticket, src/gui_2.py:App.bulk_block, src/gui_2.py:App.bulk_execute, src/gui_2.py:App.bulk_skip, tests/test_gui_phase4.py:test_push_mma_state_update]
|
||||
"""
|
||||
if not self.active_track:
|
||||
return
|
||||
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects)
|
||||
@@ -3170,7 +3419,10 @@ class AppController:
|
||||
project_manager.save_track_state(self.active_track.id, state, self.active_project_root)
|
||||
|
||||
def _load_active_tickets(self) -> None:
|
||||
"""Populates self.active_tickets based on the current execution mode."""
|
||||
"""
|
||||
Populates self.active_tickets based on the current execution mode.
|
||||
[C: tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads]
|
||||
"""
|
||||
if getattr(self, "ui_project_execution_mode", "native") == "beads":
|
||||
from src import beads_client
|
||||
bclient = beads_client.BeadsClient(Path(self.active_project_root))
|
||||
@@ -3191,4 +3443,3 @@ class AppController:
|
||||
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in self.active_track.tickets]
|
||||
else:
|
||||
self.active_tickets = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user