refactor(sdm): Refine SDM tags to 'External Only' and update core files. Pruned internal references to conserve tokens.

This commit is contained in:
2026-05-09 14:31:13 -04:00
parent e9ebcb859a
commit 696c08692e
5 changed files with 720 additions and 59 deletions
+1
View File
@@ -15,3 +15,4 @@ dpg_layout.ini
tests/temp_workspace tests/temp_workspace
.mypy_cache .mypy_cache
.slop_cache .slop_cache
sdm_report_refined.json
+173
View File
@@ -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()
+206
View File
@@ -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()
+34 -4
View File
@@ -61,6 +61,7 @@ def resolve_paths(base_dir: Path, entry: str) -> list[Path]:
def build_discussion_section(history: list[Any]) -> str: def build_discussion_section(history: list[Any]) -> str:
""" """
Builds a markdown section for discussion history. Builds a markdown section for discussion history.
Handles both legacy list[str] and new list[dict]. Handles both legacy list[str] and new list[dict].
""" """
@@ -76,6 +77,9 @@ def build_discussion_section(history: list[Any]) -> str:
return "\n\n---\n\n".join(sections) return "\n\n---\n\n".join(sections)
def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str: 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 = [] sections = []
for entry_raw in files: for entry_raw in files:
if isinstance(entry_raw, dict): if isinstance(entry_raw, dict):
@@ -120,6 +124,7 @@ 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]]: 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 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. wants to upload individual files rather than inline everything as markdown.
@@ -132,6 +137,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
tier : int | None (optional tier for context management) tier : int | None (optional tier for context management)
auto_aggregate : bool auto_aggregate : bool
force_full : 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"): with get_monitor().scope("build_file_items"):
items: list[dict[str, Any]] = [] items: list[dict[str, Any]] = []
@@ -175,6 +181,7 @@ 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: 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. Build a compact summary section using summarize.py — one short block per file.
Used as the initial <context> block instead of full file contents. Used as the initial <context> block instead of full file contents.
""" """
@@ -183,7 +190,10 @@ def build_summary_section(base_dir: Path, files: list[str | dict[str, Any]]) ->
return summarize.build_summary_markdown(items) return summarize.build_summary_markdown(items)
def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str: 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 = [] sections = []
for item in file_items: for item in file_items:
if not item.get("auto_aggregate", True): 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) return "\n\n---\n\n".join(sections)
def build_beads_section(base_dir: Path) -> str: def build_beads_section(base_dir: Path) -> str:
"""
[C: tests/test_aggregate_beads.py:test_build_beads_compaction]
"""
client = beads_client.BeadsClient(base_dir) client = beads_client.BeadsClient(base_dir)
if not client.is_initialized(): if not client.is_initialized():
return "" return ""
@@ -247,19 +260,27 @@ def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_
return "\n\n---\n\n".join(parts) 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: 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) 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: 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: if not history:
return "" return ""
return "## Discussion History\n\n" + build_discussion_section(history) 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: 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. Tier 1 Context: Strategic/Orchestration.
Full content for core conductor files and files with tier=1, summaries for others. 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"} core_files = {"product.md", "tech-stack.md", "workflow.md", "tracks.md"}
sections = [] sections = []
@@ -289,15 +310,19 @@ 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: 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. Tier 2 Context: Architectural/Tech Lead.
Full content for all files (standard behavior). 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) 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: 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. Tier 3 Context: Execution/Worker.
Full content for focus_files and files with tier=3, summaries/skeletons for others. 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"): with get_monitor().scope("build_tier3_context"):
focus_set = set(focus_files) focus_set = set(focus_files)
@@ -362,6 +387,9 @@ def build_markdown(base_dir: Path, files: list[str | dict[str, Any]], screenshot
return "\n\n---\n\n".join(parts) return "\n\n---\n\n".join(parts)
def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str, Path, list[dict[str, Any]]]: 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") namespace = config.get("project", {}).get("name")
if not namespace: if not namespace:
namespace = config.get("output", {}).get("namespace", "project") 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: def main() -> None:
# Load global config to find active project # 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 from src.paths import get_config_path
config_path = get_config_path() config_path = get_config_path()
if not config_path.exists(): if not config_path.exists():
@@ -406,4 +437,3 @@ def main() -> None:
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+274 -23
View File
@@ -42,6 +42,9 @@ from src import rag_engine
from src import theme_2 as theme from src import theme_2 as theme
def hide_tk_root() -> Tk: 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 = Tk()
root.withdraw() root.withdraw()
root.wm_attributes("-topmost", True) root.wm_attributes("-topmost", True)
@@ -49,12 +52,17 @@ def hide_tk_root() -> Tk:
def parse_symbols(text: str) -> list[str]: def parse_symbols(text: str) -> list[str]:
""" """
Finds all occurrences of '@SymbolName' in text and returns SymbolName. 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). 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) 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: 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: for file_path in files:
result = mcp_client.py_get_symbol_info(file_path, symbol) result = mcp_client.py_get_symbol_info(file_path, symbol)
if isinstance(result, tuple): if isinstance(result, tuple):
@@ -74,6 +82,9 @@ class ConfirmRequest(BaseModel):
class ConfirmDialog: class ConfirmDialog:
def __init__(self, script: str, base_dir: str) -> None: 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._uid = str(uuid.uuid4())
self._script = str(script) if script is not None else "" self._script = str(script) if script is not None else ""
self._base_dir = str(base_dir) if base_dir 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 self._approved = False
def wait(self) -> tuple[bool, str]: 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() start_time = time.time()
with self._condition: with self._condition:
while not self._done: while not self._done:
@@ -92,12 +106,18 @@ class ConfirmDialog:
class MMAApprovalDialog: class MMAApprovalDialog:
def __init__(self, ticket_id: str, payload: str) -> None: 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._payload = payload
self._condition = threading.Condition() self._condition = threading.Condition()
self._done = False self._done = False
self._approved = False self._approved = False
def wait(self) -> tuple[bool, str]: 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() start_time = time.time()
with self._condition: with self._condition:
while not self._done: while not self._done:
@@ -108,6 +128,9 @@ class MMAApprovalDialog:
class MMASpawnApprovalDialog: class MMASpawnApprovalDialog:
def __init__(self, ticket_id: str, role: str, prompt: str, context_md: str) -> None: 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._prompt = prompt
self._context_md = context_md self._context_md = context_md
self._condition = threading.Condition() self._condition = threading.Condition()
@@ -116,6 +139,9 @@ class MMASpawnApprovalDialog:
self._abort = False self._abort = False
def wait(self) -> dict[str, Any]: 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() start_time = time.time()
with self._condition: with self._condition:
while not self._done: while not self._done:
@@ -131,12 +157,16 @@ class MMASpawnApprovalDialog:
class AppController: class AppController:
""" """
The headless controller for the Manual Slop application. The headless controller for the Manual Slop application.
Owns the application state and manages background services. Owns the application state and manages background services.
""" """
def __init__(self): def __init__(self):
# Initialize locks first to avoid initialization order issues # 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._send_thread_lock: threading.Lock = threading.Lock()
self._disc_entries_lock: threading.Lock = threading.Lock() self._disc_entries_lock: threading.Lock = threading.Lock()
self._pending_comms_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 return self.ui_files_base_dir
def _update_inject_preview(self) -> None: 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: if not self._inject_file_path:
self._inject_preview = "" self._inject_preview = ""
return return
@@ -720,6 +753,9 @@ class AppController:
def _process_pending_gui_tasks(self) -> None: def _process_pending_gui_tasks(self) -> None:
# Periodic telemetry broadcast # 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() now = time.time()
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server: 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: if now - self._last_telemetry_time >= 1.0:
@@ -918,6 +954,9 @@ class AppController:
class AutoSpawnDialog: class AutoSpawnDialog:
def __init__(self, t): self.t = t def __init__(self, t): self.t = t
def wait(self): 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")} return {'approved': True, 'abort': False, 'prompt': self.t.get("prompt"), 'context_md': self.t.get("context_md")}
task["dialog_container"][0] = AutoSpawnDialog(task) task["dialog_container"][0] = AutoSpawnDialog(task)
continue continue
@@ -987,7 +1026,10 @@ class AppController:
print(f"Error executing GUI task: {e}") print(f"Error executing GUI task: {e}")
def _process_pending_history_adds(self) -> None: 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: with self._pending_history_adds_lock:
items = self._pending_history_adds[:] items = self._pending_history_adds[:]
self._pending_history_adds.clear() self._pending_history_adds.clear()
@@ -1012,7 +1054,10 @@ class AppController:
self.disc_entries.append(item) self.disc_entries.append(item)
def _process_pending_tool_calls(self) -> bool: 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: with self._pending_tool_calls_lock:
items = self._pending_tool_calls[:] items = self._pending_tool_calls[:]
self._pending_tool_calls.clear() self._pending_tool_calls.clear()
@@ -1056,7 +1101,10 @@ class AppController:
self._pending_dialog = None self._pending_dialog = None
def init_state(self): 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.active_tickets = []
self.ui_separate_task_dag = False self.ui_separate_task_dag = False
self.ui_separate_usage_analytics = False self.ui_separate_usage_analytics = False
@@ -1198,6 +1246,9 @@ class AppController:
self.event_queue.put('refresh_external_mcps', None) self.event_queue.put('refresh_external_mcps', None)
async def refresh_external_mcps(self): 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() await mcp_client.get_external_mcp_manager().stop_all()
# Start servers with auto_start=True # Start servers with auto_start=True
for name, cfg in self.mcp_config.mcpServers.items(): 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) await mcp_client.get_external_mcp_manager().add_server(cfg)
def cb_load_prior_log(self, path: Optional[str] = None) -> None: def cb_load_prior_log(self, path: Optional[str] = None) -> None:
"""
[C: src/gui_2.py:App._render_log_management]
"""
root = hide_tk_root() root = hide_tk_root()
if path is None: if path is None:
path = filedialog.askdirectory( path = filedialog.askdirectory(
@@ -1393,6 +1447,9 @@ class AppController:
def cb_exit_prior_session(self): 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 self.is_viewing_prior_session = False
if self._current_session_usage: if self._current_session_usage:
self.session_usage = self._current_session_usage self.session_usage = self._current_session_usage
@@ -1475,6 +1532,9 @@ class AppController:
thread.start() thread.start()
def _fetch_models(self, provider: str) -> None: def _fetch_models(self, provider: str) -> None:
"""
[C: src/gui_2.py:App.run]
"""
self.ai_status = "fetching models..." self.ai_status = "fetching models..."
def do_fetch() -> None: def do_fetch() -> None:
@@ -1498,14 +1558,20 @@ class AppController:
self.models_thread.start() self.models_thread.start()
def start_services(self, app: Any = None): 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._prune_old_logs()
self._init_ai_and_hooks(app) self._init_ai_and_hooks(app)
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True) self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._loop_thread.start() self._loop_thread.start()
def shutdown(self) -> None: 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 from src import ai_client
ai_client.cleanup() ai_client.cleanup()
if hasattr(self, 'hook_server') and self.hook_server: if hasattr(self, 'hook_server') and self.hook_server:
@@ -1604,7 +1670,10 @@ class AppController:
asyncio.run(self.refresh_external_mcps()) asyncio.run(self.refresh_external_mcps())
def _handle_request_event(self, event: events.UserRequestEvent) -> None: 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...' self.ai_status = 'sending...'
ai_client.set_current_tier(None) # Ensure main discussion is untagged ai_client.set_current_tier(None) # Ensure main discussion is untagged
# Clear response area for new turn # Clear response area for new turn
@@ -1662,6 +1731,9 @@ class AppController:
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"}) self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
def _on_comms_entry(self, entry: Dict[str, Any]) -> None: 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) optimized_entry = self._offload_entry_payload(entry)
session_logger.log_comms(optimized_entry) session_logger.log_comms(optimized_entry)
entry["local_ts"] = time.time() entry["local_ts"] = time.time()
@@ -1756,6 +1828,9 @@ class AppController:
self._pending_comms.append(entry) self._pending_comms.append(entry)
def _on_tool_log(self, script: str, result: str) -> None: 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_call(script, result, None)
session_logger.log_tool_output(result) session_logger.log_tool_output(result)
source_tier = ai_client.get_current_tier() 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}) 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: 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", {}) payload = kwargs.get("payload", {})
# Push to background event queue, NOT GUI queue # Push to background event queue, NOT GUI queue
self.event_queue.put("refresh_api_metrics", payload) 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]: 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): if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False):
self.ai_status = "running powershell..." self.ai_status = "running powershell..."
output = shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback) output = shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
@@ -1815,6 +1896,9 @@ class AppController:
return output return output
def _append_tool_log(self, script: str, result: str, source_tier: str | None = None, elapsed_ms: float = 0.0) -> None: 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}) self._tool_log.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
tool_name = self._extract_tool_name(script) tool_name = self._extract_tool_name(script)
is_failure = "REJECTED" in result or "Error" in result or "error" in result.lower() 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 self._token_stats_dirty = True
def create_api(self) -> FastAPI: 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 = FastAPI(title="Manual Slop Headless API")
API_KEY_NAME = "X-API-KEY" API_KEY_NAME = "X-API-KEY"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) 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)]) @api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
def get_gui_state() -> dict[str, Any]: 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", {}) gettable = getattr(self, "_gettable_fields", {})
state = {} state = {}
import dataclasses import dataclasses
@@ -1948,7 +2038,10 @@ class AppController:
@api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)]) @api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)])
def get_mma_status() -> dict[str, Any]: 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 { return {
"mma_status": self.mma_status, "mma_status": self.mma_status,
"ai_status": self.ai_status, "ai_status": self.ai_status,
@@ -1963,7 +2056,10 @@ class AppController:
@api.post("/api/gui", dependencies=[Depends(get_api_key)]) @api.post("/api/gui", dependencies=[Depends(get_api_key)])
def post_gui(req: dict) -> dict[str, str]: 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) self.event_queue.put("gui_task", req)
return {"status": "queued"} return {"status": "queued"}
@@ -1988,7 +2084,10 @@ class AppController:
@api.get("/api/performance", dependencies=[Depends(get_api_key)]) @api.get("/api/performance", dependencies=[Depends(get_api_key)])
def get_performance() -> dict[str, Any]: 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()} return {"performance": self.perf_monitor.get_metrics()}
@api.get("/api/gui/diagnostics", dependencies=[Depends(get_api_key)]) @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)]) @api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
def get_session(session_id: str) -> dict[str, Any]: 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" log_path = paths.get_logs_dir() / session_id / "comms.log"
if not log_path.exists(): if not log_path.exists():
raise HTTPException(status_code=404, detail="Session log not found") raise HTTPException(status_code=404, detail="Session log not found")
@@ -2162,15 +2264,24 @@ class AppController:
self.ai_status = "config saved" self.ai_status = "config saved"
def _cb_reset_base_prompt(self, user_data=None) -> None: 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_base_system_prompt = ai_client._SYSTEM_PROMPT
self.ui_use_default_base_prompt = False self.ui_use_default_base_prompt = False
def _cb_clear_summary_cache(self, user_data=None) -> None: def _cb_clear_summary_cache(self, user_data=None) -> None:
"""
[C: src/gui_2.py:App._render_files_panel]
"""
from src import summarize from src import summarize
summarize._summary_cache.clear() summarize._summary_cache.clear()
self._push_mma_state_update() self._push_mma_state_update()
def _cb_show_base_prompt_diff(self, user_data=None) -> None: 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 self._show_base_prompt_diff_modal = True
def _cb_disc_create(self) -> None: def _cb_disc_create(self) -> None:
@@ -2180,6 +2291,9 @@ class AppController:
self.ui_disc_new_name_input = "" self.ui_disc_new_name_input = ""
def _switch_project(self, path: str) -> None: def _switch_project(self, path: str) -> None:
"""
[C: src/gui_2.py:App._render_projects_panel]
"""
if not Path(path).exists(): if not Path(path).exists():
self.ai_status = f"project file not found: {path}" self.ai_status = f"project file not found: {path}"
return return
@@ -2200,6 +2314,9 @@ class AppController:
def _refresh_from_project(self) -> None: def _refresh_from_project(self) -> None:
# Deserialize FileItems in files.paths # 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", []) raw_paths = self.project.get("files", {}).get("paths", [])
self.files = [] self.files = []
for p in raw_paths: for p in raw_paths:
@@ -2279,6 +2396,9 @@ class AppController:
self._rebuild_rag_index() self._rebuild_rag_index()
def _cb_save_workspace_profile(self, name: str, scope: str = 'project') -> None: 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: if not hasattr(self, '_app') or not self._app:
return return
profile = self._app._capture_workspace_profile(name) profile = self._app._capture_workspace_profile(name)
@@ -2287,18 +2407,27 @@ class AppController:
self._app.workspace_profiles = self.workspace_profiles self._app.workspace_profiles = self.workspace_profiles
def _cb_delete_workspace_profile(self, name: str, scope: str = 'project') -> None: 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_manager.delete_profile(name, scope=scope)
self.workspace_profiles = self.workspace_manager.load_all_profiles() self.workspace_profiles = self.workspace_manager.load_all_profiles()
if hasattr(self, '_app') and self._app: if hasattr(self, '_app') and self._app:
self._app.workspace_profiles = self.workspace_profiles self._app.workspace_profiles = self.workspace_profiles
def _cb_load_workspace_profile(self, name: str) -> None: def _cb_load_workspace_profile(self, name: str) -> None:
"""
[C: src/gui_2.py:App._show_menus]
"""
if name in self.workspace_profiles: if name in self.workspace_profiles:
profile = self.workspace_profiles[name] profile = self.workspace_profiles[name]
if hasattr(self, '_app') and self._app: if hasattr(self, '_app') and self._app:
self._app._apply_workspace_profile(profile) self._app._apply_workspace_profile(profile)
def _apply_preset(self, name: str, scope: str) -> None: 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}") print(f"[DEBUG] _apply_preset: name={name}, scope={scope}")
if name == "None": if name == "None":
if scope == "global": if scope == "global":
@@ -2318,6 +2447,9 @@ class AppController:
self.ui_project_preset_name = name self.ui_project_preset_name = name
def _cb_save_preset(self, name, content, scope): 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}") print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}")
if not name or not name.strip(): if not name or not name.strip():
raise ValueError("Preset name cannot be empty or whitespace.") 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)}") print(f"[DEBUG] _cb_save_preset: saved {name}, total presets now {len(self.presets)}")
def _cb_delete_preset(self, name, scope): 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.preset_manager.delete_preset(name, scope)
self.presets = self.preset_manager.load_all() self.presets = self.preset_manager.load_all()
def _cb_save_tool_preset(self, name, categories, scope): 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) preset = models.ToolPreset(name=name, categories=categories)
self.tool_preset_manager.save_preset(preset, scope) self.tool_preset_manager.save_preset(preset, scope)
self.tool_presets = self.tool_preset_manager.load_all_presets() self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_delete_tool_preset(self, name, scope): 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_preset_manager.delete_preset(name, scope)
self.tool_presets = self.tool_preset_manager.load_all_presets() self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"): 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.tool_preset_manager.save_bias_profile(profile, scope)
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles() 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() self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None: 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.persona_manager.save_persona(persona, scope)
self.personas = self.persona_manager.load_all() self.personas = self.persona_manager.load_all()
def _cb_delete_persona(self, name: str, scope: str = "project") -> None: 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.persona_manager.delete_persona(name, scope)
self.personas = self.persona_manager.load_all() self.personas = self.persona_manager.load_all()
def _cb_load_track(self, track_id: str) -> None: 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) state = project_manager.load_track_state(track_id, self.active_project_root)
if state: if state:
try: try:
@@ -2391,6 +2544,9 @@ class AppController:
print(f"Error loading track {track_id}: {e}") print(f"Error loading track {track_id}: {e}")
def _save_active_project(self) -> None: 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: if self.active_project_path:
try: try:
cleaned = project_manager.clean_nones(self.project) cleaned = project_manager.clean_nones(self.project)
@@ -2399,11 +2555,17 @@ class AppController:
self.ai_status = f"save error: {e}" self.ai_status = f"save error: {e}"
def _get_discussion_names(self) -> list[str]: def _get_discussion_names(self) -> list[str]:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
disc_sec = self.project.get("discussion", {}) disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {}) discussions = disc_sec.get("discussions", {})
return sorted(discussions.keys()) return sorted(discussions.keys())
def _switch_discussion(self, name: str) -> None: 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() self._flush_disc_entries_to_project()
disc_sec = self.project.get("discussion", {}) disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {}) discussions = disc_sec.get("discussions", {})
@@ -2419,6 +2581,9 @@ class AppController:
self.ai_status = f"discussion: {name}" self.ai_status = f"discussion: {name}"
def _flush_disc_entries_to_project(self) -> None: 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] history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
if self.active_track and self._track_discussion_active: if self.active_track and self._track_discussion_active:
project_manager.save_track_history(self.active_track.id, history_strings, self.active_project_root) 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() disc_data["last_updated"] = project_manager.now_ts()
def _create_discussion(self, name: str) -> None: 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", {}) disc_sec = self.project.setdefault("discussion", {})
discussions = disc_sec.setdefault("discussions", {}) discussions = disc_sec.setdefault("discussions", {})
if name in discussions: if name in discussions:
@@ -2439,6 +2607,9 @@ class AppController:
self._switch_discussion(name) self._switch_discussion(name)
def _branch_discussion(self, index: int) -> None: def _branch_discussion(self, index: int) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
self._flush_disc_entries_to_project() self._flush_disc_entries_to_project()
# Generate a unique branch name # Generate a unique branch name
base_name = self.active_discussion.split("_take_")[0] 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) project_manager.branch_discussion(self.project, self.active_discussion, new_name, index)
self._switch_discussion(new_name) self._switch_discussion(new_name)
def _rename_discussion(self, old_name: str, new_name: str) -> None: 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", {}) disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {}) discussions = disc_sec.get("discussions", {})
if old_name not in discussions: if old_name not in discussions:
@@ -2466,6 +2640,9 @@ class AppController:
disc_sec["active"] = new_name disc_sec["active"] = new_name
def _delete_discussion(self, name: str) -> None: def _delete_discussion(self, name: str) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
disc_sec = self.project.get("discussion", {}) disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {}) discussions = disc_sec.get("discussions", {})
if len(discussions) <= 1: if len(discussions) <= 1:
@@ -2479,6 +2656,9 @@ class AppController:
self._switch_discussion(remaining[0]) 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: 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: if self._pending_mma_approvals:
task = self._pending_mma_approvals.pop(0) task = self._pending_mma_approvals.pop(0)
dlg = task.get("dialog_container", [None])[0] dlg = task.get("dialog_container", [None])[0]
@@ -2504,7 +2684,10 @@ class AppController:
spawn_dlg._condition.notify_all() spawn_dlg._condition.notify_all()
def _handle_approve_ask(self) -> None: 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 if not self._ask_request_id: return
request_id = self._ask_request_id request_id = self._ask_request_id
@@ -2522,7 +2705,10 @@ class AppController:
self._ask_tool_data = None self._ask_tool_data = None
def _handle_reject_ask(self) -> 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 if not self._ask_request_id: return
request_id = self._ask_request_id request_id = self._ask_request_id
@@ -2540,7 +2726,10 @@ class AppController:
self._ask_tool_data = None self._ask_tool_data = None
def _handle_reset_session(self) -> 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.reset_session()
ai_client.clear_comms_log() ai_client.clear_comms_log()
self._tool_log.clear() self._tool_log.clear()
@@ -2568,9 +2757,15 @@ class AppController:
self._pending_gui_tasks.clear() self._pending_gui_tasks.clear()
def _handle_md_only(self) -> None: 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(): 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: try:
md, path, *_ = self._do_generate() md, path, *_ = self._do_generate()
self.last_md = md self.last_md = md
@@ -2583,9 +2778,15 @@ class AppController:
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
def _handle_generate_send(self) -> None: 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(): 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.write("[DEBUG] _handle_generate_send worker started\n")
sys.stderr.flush() sys.stderr.flush()
try: try:
@@ -2650,6 +2851,9 @@ class AppController:
self._cached_files = stats.get("cached_files", []) self._cached_files = stats.get("cached_files", [])
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None: 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: if "latency" in payload:
self.session_usage["last_latency"] = payload["latency"] self.session_usage["last_latency"] = payload["latency"]
if "usage" in payload and "percentage" in payload["usage"]: if "usage" in payload and "percentage" in payload["usage"]:
@@ -2674,11 +2878,17 @@ class AppController:
self._cached_tool_stats = dict(self._tool_stats) self._cached_tool_stats = dict(self._tool_stats)
def clear_cache(self) -> None: def clear_cache(self) -> None:
"""
[C: src/gui_2.py:App._render_cache_panel]
"""
from src import ai_client from src import ai_client
ai_client.cleanup() ai_client.cleanup()
self._update_cached_stats() self._update_cached_stats()
def get_session_insights(self) -> Dict[str, Any]: def get_session_insights(self) -> Dict[str, Any]:
"""
[C: src/gui_2.py:App._render_session_insights_panel]
"""
from src import cost_tracker from src import cost_tracker
total_input = sum(e["input"] for e in self._token_history) total_input = sum(e["input"] for e in self._token_history)
total_output = sum(e["output"] 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: 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 = self.project
proj.setdefault("output", {})["output_dir"] = self.ui_output_dir proj.setdefault("output", {})["output_dir"] = self.ui_output_dir
proj.setdefault("files", {})["base_dir"] = self.ui_files_base_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) project_manager.save_project(cleaned_proj, self.active_project_path)
def _flush_to_config(self) -> None: 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"] = { self.config["ai"] = {
"provider": self.current_provider, "provider": self.current_provider,
"model": self.current_model, "model": self.current_model,
@@ -2778,7 +2994,10 @@ class AppController:
theme.save_to_config(self.config) theme.save_to_config(self.config)
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], 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).
[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_project()
self._flush_to_config() self._flush_to_config()
models.save_config(self.config) models.save_config(self.config)
@@ -2809,6 +3028,9 @@ class AppController:
return full_md, path, file_items, stable_md, discussion_text return full_md, path, file_items, stable_md, discussion_text
def _cb_plan_epic(self) -> None: 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: def _bg_task() -> None:
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n") sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
sys.stderr.flush() sys.stderr.flush()
@@ -2855,6 +3077,9 @@ class AppController:
threading.Thread(target=_bg_task, daemon=True).start() threading.Thread(target=_bg_task, daemon=True).start()
def _cb_accept_tracks(self) -> None: def _cb_accept_tracks(self) -> None:
"""
[C: src/gui_2.py:App._render_track_proposal_modal]
"""
self._show_track_proposal_modal = False self._show_track_proposal_modal = False
def _bg_task() -> None: def _bg_task() -> None:
@@ -2896,6 +3121,9 @@ class AppController:
threading.Thread(target=_bg_task, daemon=True).start() threading.Thread(target=_bg_task, daemon=True).start()
def _cb_start_track(self, user_data: Any = None) -> None: 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 isinstance(user_data, str):
# If track_id is provided directly # If track_id is provided directly
track_id = user_data track_id = user_data
@@ -3005,6 +3233,9 @@ class AppController:
print(f"ERROR in _start_track_logic: {e}") print(f"ERROR in _start_track_logic: {e}")
def _cb_ticket_retry(self, ticket_id: str) -> None: 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: for t in self.active_tickets:
if t.get('id') == ticket_id: if t.get('id') == ticket_id:
t['status'] = 'todo' t['status'] = 'todo'
@@ -3012,6 +3243,9 @@ class AppController:
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}) self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
def _cb_ticket_skip(self, ticket_id: str) -> None: 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: for t in self.active_tickets:
if t.get('id') == ticket_id: if t.get('id') == ticket_id:
t['status'] = 'skipped' t['status'] = 'skipped'
@@ -3031,7 +3265,10 @@ class AppController:
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}) self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
def kill_worker(self, worker_id: str) -> None: 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) engine = self.engines.get(self.active_track.id if self.active_track else None)
if engine: if engine:
engine.kill_worker(worker_id) engine.kill_worker(worker_id)
@@ -3051,7 +3288,10 @@ class AppController:
engine.resume() engine.resume()
def inject_context(self, data: dict) -> None: 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") file_path = data.get("file_path")
if file_path: if file_path:
if not os.path.isabs(file_path): if not os.path.isabs(file_path):
@@ -3097,6 +3337,9 @@ class AppController:
self._push_mma_state_update() self._push_mma_state_update()
def _cb_run_conductor_setup(self) -> None: 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) base = paths.get_conductor_dir(project_path=self.active_project_root)
if not base.exists(): if not base.exists():
self.ui_conductor_setup_summary = f"Error: {base}/ directory not found." 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) self.ui_conductor_setup_summary = "\n".join(summary)
def _cb_create_track(self, name: str, desc: str, track_type: str) -> None: 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 if not name: return
date_suffix = datetime.now().strftime("%Y%m%d") date_suffix = datetime.now().strftime("%Y%m%d")
track_id = f"{name.lower().replace(' ', '_')}_{date_suffix}" 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) self.tracks = project_manager.get_all_tracks(self.active_project_root)
def _push_mma_state_update(self) -> None: 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: if not self.active_track:
return return
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects) # 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) project_manager.save_track_state(self.active_track.id, state, self.active_project_root)
def _load_active_tickets(self) -> None: 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": if getattr(self, "ui_project_execution_mode", "native") == "beads":
from src import beads_client from src import beads_client
bclient = beads_client.BeadsClient(Path(self.active_project_root)) 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] self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in self.active_track.tickets]
else: else:
self.active_tickets = [] self.active_tickets = []