from __future__ import annotations

import copy
import inspect
import json
import os
import re
import signal
import sys
import threading
import time
import traceback
import uuid

# TODO(Ed): Eliminate these?
from dataclasses import asdict
from datetime    import datetime
from pathlib     import Path
from typing      import Any, List, Dict, Optional, Callable

from src import aggregate
from src import models
from src import ai_client
from src.module_loader import _require_warmed
from src import conductor_tech_lead
from src import events
from src import mcp_client
from src import multi_agent_conductor
from src import orchestrator_pm
from src import paths
from src import performance_monitor
from src import project_manager
from src import session_logger
from src import workspace_manager
from src import presets
from src import shell_runner
from src import theme_2 as theme
from src import thinking_parser
from src import tool_presets

from src.context_presets import ContextPresetManager
from src.file_cache      import ASTParser
from src.io_pool         import make_io_pool
from src.models          import GenerateRequest, ConfirmRequest
from src.warmup          import WarmupManager


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).
 """
 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:
 for file_path in files:
  result = mcp_client.py_get_symbol_info(file_path, symbol)
  if isinstance(result, tuple):
   source, line = result
   return (file_path, source, line)
 return None

class ConfirmDialog:
 def __init__(self, script: str, base_dir: str) -> None:
  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 ""
  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_ai_server.py:test_server_handles_list_models, tests/test_ai_server.py:test_server_handles_unknown_method, tests/test_ai_server.py:test_server_loads_google_genai_quickly, tests/test_ai_server.py:test_server_outputs_ready_marker, tests/test_ai_server.py:test_server_starts_and_exits_cleanly, 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:
    if time.time() - start_time > 120:
     return False, self._script
    self._condition.wait(timeout=0.1)
  return self._approved, self._script

#region: API Handlers

async def _api_get_key(controller: 'AppController', header_key: str) -> str:
 """
 Validates the API key from the request header against configuration.
 [SDM: src/app_controller.py:_api_get_key]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 headless_cfg = controller.config.get("headless", {})
 config_key = headless_cfg.get("api_key", "").strip()
 env_key = os.environ.get("SLOP_API_KEY", "").strip()
 target_key = env_key or config_key
 if not target_key:
  raise HTTPException(status_code=403, detail="API Key not configured on server")
 if header_key == target_key:
  return header_key
 raise HTTPException(status_code=403, detail="Could not validate API Key")

def _api_health(controller: 'AppController') -> dict[str, str]:
 """
 Returns the health status of the API.
 [SDM: src/app_controller.py:_api_health]
 """
 return {"status": "ok"}

def _api_get_gui_state(controller: 'AppController') -> dict[str, Any]:
 """
 Returns the current GUI state for specific fields.
 [SDM: src/app_controller.py:_api_get_gui_state]
 """
 gettable = getattr(controller, "_gettable_fields", {})
 state = {}
 import dataclasses
 for key, attr in gettable.items():
  val = getattr(controller, attr, None)
  if dataclasses.is_dataclass(val):
   state[key] = dataclasses.asdict(val)
  else:
   state[key] = val

 # Compatibility overrides
 show_windows = getattr(controller, "show_windows", {})
 state["show_text_viewer"] = show_windows.get("Text Viewer", False)

 return state

def _api_get_mma_status(controller: 'AppController') -> dict[str, Any]:
 """
 Dedicated endpoint for MMA-related status.
 [SDM: src/app_controller.py:_api_get_mma_status]
 """
 return {
  "mma_status":      controller.mma_status,
  "ai_status":       controller.ai_status,
  "mma_streams":     controller.mma_streams,
  "worker_status":   controller._worker_status,
  "tool_stats":      controller._tool_stats,
  "active_tier":     controller.active_tier,
  "active_tickets":  controller.active_tickets,
  "proposed_tracks": controller.proposed_tracks,
  "tracks":          controller.tracks,
  "tier_usage":      controller.mma_tier_usage
 }

def _api_post_gui(controller: 'AppController', req: dict) -> dict[str, str]:    
 """
 Pushes a GUI task to the event queue.
 [SDM: src/app_controller.py:_api_post_gui]
 """
 controller.event_queue.put("gui_task", req)
 return {"status": "queued"}

def _api_get_api_session(controller: 'AppController') -> dict[str, Any]:        
 """
  Returns current discussion session entries.
  [SDM: src/app_controller.py:_api_get_api_session]
 """
 with controller._disc_entries_lock:
  return {"session": {"entries": controller.disc_entries}}

def _api_post_api_session(controller: 'AppController', req: dict) -> dict[str, str]:
 """
  Updates session entries.
  [SDM: src/app_controller.py:_api_post_api_session]
 """
 entries = req.get("entries", [])
 with controller._disc_entries_lock:
  controller.disc_entries = entries
 return {"status": "updated"}

def _api_get_api_project(controller: 'AppController') -> dict[str, Any]:        
 """
  Returns current project data.
  [SDM: src/app_controller.py:_api_get_api_project]
 """
 return {"project": controller.project}

def _api_get_performance(controller: 'AppController') -> dict[str, Any]:        
 """
  Returns performance monitor metrics.
  [SDM: src/app_controller.py:_api_get_performance]
 """
 return {"performance": controller.perf_monitor.get_metrics()}

def _api_get_diagnostics(controller: 'AppController') -> dict[str, Any]:        
 """
  Alias for performance metrics.
  [SDM: src/app_controller.py:_api_get_diagnostics]
 """
 return controller.perf_monitor.get_metrics()

def _api_status(controller: 'AppController') -> dict[str, Any]:
 """
  Returns the current status of the application.
  [SDM: src/app_controller.py:_api_status]
 """
 return {
  "provider": controller.current_provider,
  "model": controller.current_model,
  "status": controller.ai_status,
  "usage": controller.session_usage
 }

def _api_generate(controller: 'AppController', req: GenerateRequest) -> dict[str, Any]:
 """
  Triggers an AI generation request using the current project context.
  [SDM: src/app_controller.py:_api_generate]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 if not req.prompt.strip():
  raise HTTPException(status_code=400, detail="Prompt cannot be empty")
 if controller.is_project_stale():
  raise HTTPException(status_code=409, detail="Project switch in progress; AI ops are disabled until it completes.")
 with controller._send_thread_lock:
  start_time = time.time()
  try:
   # 1. Build context
   from src import aggregate
   full_md, path, file_items, stable_md, disc_text = controller._do_generate()
   controller._last_stable_md = stable_md
   controller.last_md = full_md
   controller.last_md_path = path
   controller.last_file_items = file_items
   
   user_msg = req.prompt
   
   # 2. RAG Retrieval
   if controller.rag_engine and controller.rag_config and controller.rag_config.enabled:
    try:
     chunks = controller.rag_engine.search(user_msg)
     if chunks:
      context_block = "## Retrieved Context\n\n"
      for i, chunk in enumerate(chunks):
       path = chunk.get("metadata", {}).get("path", "unknown")
       context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n"
      user_msg = context_block + user_msg
    except Exception as e:
     sys.stderr.write(f"RAG search error: {e}\n")
     sys.stderr.flush()

   # 3. Symbol Resolution
   try:
    from src.markdown_helper import parse_symbols, get_symbol_definition
    symbols = parse_symbols(user_msg)
    file_paths = [f.path if hasattr(f, "path") else f.get("path") if isinstance(f, dict) else str(f) for f in controller.last_file_items]
    for symbol in symbols:
     res = get_symbol_definition(symbol, file_paths)
     if res:
      file_path, definition, line = res
      user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```'
   except Exception as e:
    sys.stderr.write(f"Symbol resolution error: {e}\n")
    sys.stderr.flush()

   base_dir = controller.active_project_root
   csp = filter(bool, [controller.ui_global_system_prompt.strip(), controller.ui_project_system_prompt.strip()])
   ai_client.set_custom_system_prompt("\n\n".join(csp))
   ai_client.set_base_system_prompt(controller.ui_base_system_prompt)
   ai_client.set_use_default_base_prompt(controller.ui_use_default_base_prompt)  
   ai_client.set_project_context_marker(controller.ui_project_context_marker)    
   temp = req.temperature if req.temperature is not None else controller.temperature
   top_p = req.top_p if req.top_p is not None else controller.top_p
   tokens = req.max_tokens if req.max_tokens is not None else controller.max_tokens
   ai_client.set_model_params(temp, tokens, controller.history_trunc_limit, top_p)
   ai_client.set_agent_tools(controller.ui_agent_tools)
   
   if req.auto_add_history:
    with controller._pending_history_adds_lock:
     controller._pending_history_adds.append({
      "role": "User",
      "content": user_msg,
      "collapsed": True,
      "ts": project_manager.now_ts()
      })
      
   with controller._disc_entries_lock:
    has_ai_response = any(e.get("role") == "AI" for e in controller.disc_entries)
   context_to_send = stable_md if not has_ai_response else ""

   result = ai_client.send_result(context_to_send, user_msg, base_dir, controller.last_file_items, disc_text, rag_engine=None)
   if not result.ok:
    err = result.errors[0]
    raise HTTPException(status_code=502, detail=err.ui_message())
   resp = result.data

   if req.auto_add_history:
    with controller._pending_history_adds_lock:
     controller._pending_history_adds.append({
      "role": "AI",
      "content": resp,
      "collapsed": True,
      "ts": project_manager.now_ts()
      })

   controller._recalculate_session_usage()
   duration = time.time() - start_time
   return {
    "text": resp,
    "metadata": {
     "provider": controller.current_provider,
     "model": controller.current_model,
     "duration_sec": round(duration, 3),
     "timestamp": project_manager.now_ts()
    },
    "usage": controller.session_usage
   }
  except Exception as e:
   import traceback
   traceback.print_exc()
   raise HTTPException(status_code=500, detail=str(e))

async def _api_stream(controller: 'AppController', req: GenerateRequest) -> Any:
 """
  Placeholder for streaming AI generation responses (Not yet implemented).       
  [SDM: src/app_controller.py:_api_stream]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 raise HTTPException(status_code=501, detail="Streaming endpoint (/api/v1/stream) is not yet supported in this version.")

def _api_pending_actions(controller: 'AppController') -> list[dict[str, Any]]:  
 """
  Lists all pending PowerShell scripts awaiting confirmation.
  [SDM: src/app_controller.py:_api_pending_actions]
 """
 with controller._pending_dialog_lock:
  return [
   {"action_id": uid, "script": diag._script, "base_dir": diag._base_dir}       
   for uid, diag in controller._pending_actions.items()
  ]

def _api_confirm_action(controller: 'AppController', action_id: str, req: ConfirmRequest) -> dict[str, str]:
 """
  Approves or rejects a pending action.
  [SDM: src/app_controller.py:_api_confirm_action]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 with controller._pending_dialog_lock:
  if action_id not in controller._pending_actions:
   raise HTTPException(status_code=404, detail="Action not found")
  dialog = controller._pending_actions.pop(action_id)
  if req.script is not None:
   dialog._script = req.script
  with dialog._condition:
   dialog._approved = req.approved
   dialog._done = True
   dialog._condition.notify_all()
  return {"status": "confirmed" if req.approved else "rejected"}

def _api_list_sessions(controller: 'AppController') -> list[str]:
 """
  Lists all session IDs.
  [SDM: src/app_controller.py:_api_list_sessions]
 """
 log_dir = paths.get_logs_dir()
 if not log_dir.exists():
  return []
 return [d.name for d in log_dir.iterdir() if d.is_dir()]

def _api_get_session(controller: 'AppController', session_id: str) -> dict[str, Any]:
 """
  Returns the content of the comms.log for a specific session.
  [SDM: src/app_controller.py:_api_get_session]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 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")
 return {"id": session_id, "content": log_path.read_text(encoding="utf-8", errors="replace")}

def _api_delete_session(controller: 'AppController', session_id: str) -> dict[str, str]:
 """
  Deletes a specific session directory.
  [SDM: src/app_controller.py:_api_delete_session]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 log_path = paths.get_logs_dir() / session_id
 if not log_path.exists() or not log_path.is_dir():
  raise HTTPException(status_code=404, detail="Session directory not found")    
 import shutil
 shutil.rmtree(log_path)
 return {"status": "deleted"}

def _api_get_context(controller: 'AppController') -> dict[str, Any]:
 """
  Returns the current aggregated project context.
  [SDM: src/app_controller.py:_api_get_context]
 """
 HTTPException = _require_warmed("fastapi").HTTPException
 try:
  md, path, file_items, stable_md, disc_text = controller._do_generate()        
  screenshots = controller.project.get("screenshots", {}).get("paths", [])      
  return {
   "files": [f.get("path") if isinstance(f, dict) else str(f) for f in file_items],
   "screenshots": screenshots,
   "files_base_dir": controller.active_project_root,
   "markdown": md,
   "discussion": disc_text
  }
 except Exception as e:
  raise HTTPException(status_code=500, detail=f"Context aggregation failure: {e}")

def _api_token_stats(controller: 'AppController') -> dict[str, Any]:
 """
  Returns current token usage and budget statistics.
  [SDM: src/app_controller.py:_api_token_stats]
 """
 return controller._token_stats

#endregion: API Handlers

#region: GUI Task Handlers

def _handle_ai_response(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_ai_response]"""
 payload = task.get("payload", {})
 text = payload.get("text", "")
 stream_id = payload.get("stream_id")
 is_streaming = payload.get("status") == "streaming..."
 if stream_id:
  if is_streaming:
   if stream_id not in controller.mma_streams: controller.mma_streams[stream_id] = ""
   if stream_id not in controller._worker_status: controller._worker_status[stream_id] = "running"
   controller.mma_streams[stream_id] += text
   if len(controller.mma_streams[stream_id]) > controller.MAX_STREAM_SIZE:
    controller.mma_streams[stream_id] = controller.mma_streams[stream_id][-controller.MAX_STREAM_SIZE:]
  else:
   controller.mma_streams[stream_id] = text
   if stream_id in controller._worker_status and controller._worker_status[stream_id] == "running":
    controller._worker_status[stream_id] = "completed"
  if stream_id == "Tier 1":
   if "status" in payload:
    controller._ai_status = payload["status"]
 else:
  if is_streaming:
   controller.ai_response += text
  else:
   controller.ai_response = text
  controller._ai_status = payload.get("status", "done")
 controller._trigger_blink = True
 if not stream_id:
  controller._token_stats_dirty = True
  if not is_streaming:
   controller._autofocus_response_tab = True

def _handle_mma_state_update(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_mma_state_update]"""
 p = task.get("payload")
 if not isinstance(p, dict):
  p = task 

 track_data = p.get("track")
 is_active_track = False
 if track_data and controller.active_track and track_data.get("id") == controller.active_track.id:
  is_active_track = True

 if is_active_track or not controller.active_track or not track_data:
  controller._mma_status = p.get("status", controller._mma_status)

  old_tier = controller.active_tier
  controller.active_tier = p.get("active_tier", controller.active_tier)

  if getattr(controller, "ui_auto_switch_layout", False) and controller.active_tier and controller.active_tier != old_tier:
   for tier_prefix in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]:
    if controller.active_tier.startswith(tier_prefix):
     bound_profile = getattr(controller, "ui_tier_layout_bindings", {}).get(tier_prefix)
     if bound_profile:
      controller._cb_load_workspace_profile(bound_profile)
     break

 new_usage = p.get("tier_usage", {})
 for tier, data in new_usage.items():
  if tier in controller.mma_tier_usage:
   input_val = data.get("input")
   if input_val is not None:
    controller.mma_tier_usage[tier]["input"] = input_val

   output_val = data.get("output")
   if output_val is not None:
    controller.mma_tier_usage[tier]["output"] = output_val
   if "model" in data: controller.mma_tier_usage[tier]["model"] = data["model"]
   if "provider" in data: controller.mma_tier_usage[tier]["provider"] = data["provider"]
  else:
   controller.mma_tier_usage[tier] = data

 if is_active_track or not controller.active_track or not track_data:
  controller.active_tickets = p.get("tickets", [])
  if track_data:
   tickets = []
   for t_data in controller.active_tickets:
    if isinstance(t_data, models.Ticket):
     tickets.append(t_data)
    else:
     if "goal" in t_data and "description" not in t_data:
      t_data["description"] = t_data["goal"]
     tickets.append(models.Ticket.from_dict(t_data))
   controller.active_track = models.Track(
    id=track_data.get("id"),
    description=track_data.get("title", ""),
    tickets=tickets
   )

def _handle_refresh_api_metrics(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_refresh_api_metrics]"""
 controller._refresh_api_metrics(task.get("payload", {}), md_content=controller.last_md or None)

def _handle_set_ai_status(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_set_ai_status]"""
 controller._ai_status = task.get("value", task.get("payload", ""))

def _handle_set_mma_status(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_set_mma_status]"""
 controller._mma_status = task.get("value", task.get("payload", ""))

def _handle_mma_stream(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_mma_stream]"""
 stream_id = task.get("stream_id") or task.get("payload", {}).get("stream_id")
 text = task.get("text") or task.get("payload", {}).get("text", "")
 if stream_id:
  if stream_id not in controller.mma_streams:
   controller.mma_streams[stream_id] = ""
  controller.mma_streams[stream_id] += text

def _handle_show_track_proposal(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_show_track_proposal]"""
 controller.proposed_tracks = task.get("payload", [])
 controller._show_track_proposal_modal = True

def _handle_custom_callback(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_custom_callback]"""
 cb = task.get("callback")
 args = task.get("args", [])
 if callable(cb):
  try:
   cb(*args)
  except Exception as e:
   print(f"Error in direct custom callback: {e}")
 elif cb in controller._predefined_callbacks:
  controller._predefined_callbacks[cb](*args)

def _handle_set_value(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_set_value]"""
 item = task.get("item")
 value = task.get("value")
 if item in controller._settable_fields:
  attr_name = controller._settable_fields[item]
  setattr(controller, attr_name, value)
  if item == "gcli_path":
   controller._update_gcli_adapter(str(value))
  return
 # Dict-key bracket notation: e.g. 'show_windows["Project Settings"]'
 if "[" in item and item.endswith("]"):
  dict_name, _, key_part = item.partition("[")
  key = key_part[:-1].strip().strip("'\"")
  if dict_name in controller._settable_fields:
   attr_name = controller._settable_fields[dict_name]
   current = getattr(controller, attr_name, None)
   if isinstance(current, dict):
    new_dict = {**current, key: value}
    setattr(controller, attr_name, new_dict)

def _handle_click(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_click]"""
 item = task.get("item")
 user_data = task.get("user_data")
 if item == "btn_project_new_automated":
  controller._cb_new_project_automated(user_data)
 elif item == "btn_mma_load_track":
  controller._cb_load_track(str(user_data or ""))
 elif item in controller._clickable_actions:
  func = controller._clickable_actions[item]
  try:
   sig = inspect.signature(func)
   if 'user_data' in sig.parameters:
    func(user_data=user_data)
   else:
    func()
  except Exception:
   func()

def _handle_drag(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_drag]"""
 src_item = task.get("src_item")
 dst_item = task.get("dst_item")
 if src_item in controller._drag_actions:
  func = controller._drag_actions[src_item]
  func(dst_item=dst_item)

def _handle_right_click(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_right_click]"""
 item = task.get("item")
 if item in controller._right_clickable_actions:
  func = controller._right_clickable_actions[item]
  func()

def _handle_select_list_item(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_select_list_item]"""
 item = task.get("listbox", task.get("item"))
 value = task.get("item_value", task.get("value"))
 if item == "disc_listbox":
  controller._switch_discussion(str(value or ""))

def _handle_ask(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_ask]"""
 controller._pending_ask_dialog = True
 controller._ask_request_id = task.get("request_id")
 controller._ask_tool_data = task.get("data", {})

def _handle_clear_ask(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_clear_ask]"""
 if controller._ask_request_id == task.get("request_id"):
  controller._pending_ask_dialog = False
  controller._ask_request_id = None
  controller._ask_tool_data = None

def _handle_mma_step_approval(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_mma_step_approval]"""
 if controller.test_hooks_enabled and not getattr(controller, "ui_manual_approve", False):
  if "dialog_container" in task:
   class AutoStepDialog:
    def __init__(self, t): self.t = t
    def wait(self): return True, self.t.get("payload", "")
   task["dialog_container"][0] = AutoStepDialog(task)
  return
 dlg = MMAApprovalDialog(str(task.get("ticket_id") or ""), str(task.get("payload") or ""))
 controller._pending_mma_approvals.append(task)
 if "dialog_container" in task:
  task["dialog_container"][0] = dlg

def _handle_mma_spawn_approval(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_mma_spawn_approval]"""
 if controller.test_hooks_enabled and not getattr(controller, "ui_manual_approve", False):
  if "dialog_container" in task:
   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_ai_server.py:test_server_handles_list_models, tests/test_ai_server.py:test_server_handles_unknown_method, tests/test_ai_server.py:test_server_loads_google_genai_quickly, tests/test_ai_server.py:test_server_outputs_ready_marker, tests/test_ai_server.py:test_server_starts_and_exits_cleanly, 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)
  return
 spawn_dlg = MMASpawnApprovalDialog(
  str(task.get("ticket_id") or ""), 
  str(task.get("role") or ""), 
  str(task.get("prompt") or ""), 
  str(task.get("context_md") or "")
 )
 controller._pending_mma_spawns.append(task)
 controller._mma_spawn_prompt = task.get("prompt", "")
 controller._mma_spawn_context = task.get("context_md", "")
 controller._mma_spawn_open = True
 controller._mma_spawn_edit_mode = False
 if "dialog_container" in task:
  task["dialog_container"][0] = spawn_dlg

def _handle_ticket_started(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_ticket_started]"""
 payload = task.get("payload", {})
 ticket_id = payload.get("ticket_id")
 start_time = payload.get("timestamp")
 persona_id = payload.get("persona_id")
 model = payload.get("model")
 if ticket_id and start_time:
  controller._ticket_start_times[ticket_id] = start_time
 if ticket_id and (persona_id or model):
  stream_id = f"Tier 3 (Worker): {ticket_id}"
  meta_info = f"[STARTED] Ticket: {ticket_id}"
  if model:
   meta_info += f" | Model: {model}"
  if persona_id:
   meta_info += f" | Persona: {persona_id}"
  meta_info += "\n" + "="*50 + "\n"
  if stream_id not in controller.mma_streams:
   controller.mma_streams[stream_id] = ""
  controller.mma_streams[stream_id] = meta_info + controller.mma_streams[stream_id]

def _handle_ticket_completed(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_ticket_completed]"""
 payload = task.get("payload", {})
 ticket_id = payload.get("ticket_id")
 end_time = payload.get("timestamp")
 if ticket_id and end_time and ticket_id in controller._ticket_start_times:
  start_time = controller._ticket_start_times.pop(ticket_id)
  elapsed = end_time - start_time
  controller._completed_ticket_count += 1
  controller._avg_ticket_time = ((controller._avg_ticket_time * (controller._completed_ticket_count - 1)) + elapsed) / controller._completed_ticket_count

def _handle_bead_updated(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_bead_updated]"""
 payload = task.get("payload", {})
 bead_id = payload.get("bead_id") or payload.get("bid")
 status = payload.get("status")
 if bead_id and status:
  stream_id = "Tier 2 (Tech Lead)"
  msg = f"\n[BEAD UPDATE] {bead_id} -> status: {status}\n"
  if stream_id not in controller.mma_streams:
   controller.mma_streams[stream_id] = ""
  controller.mma_streams[stream_id] += msg

def _handle_set_comms_dirty(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_set_comms_dirty]"""
 controller._comms_log_dirty = True

def _handle_set_tool_log_dirty(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_set_tool_log_dirty]"""
 controller._tool_log_dirty = True

def _handle_refresh_from_project(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_refresh_from_project]"""
 controller._refresh_from_project()

def _handle_show_patch_modal(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_show_patch_modal]"""
 controller._pending_patch_text = task.get("patch_text", "")
 controller._pending_patch_files = task.get("file_paths", [])
 controller._show_patch_modal = True

def _handle_hide_patch_modal(controller: 'AppController', task: dict):
 """[SDM: AppController._handle_hide_patch_modal]"""
 controller._show_patch_modal = False
 controller._pending_patch_text = None
 controller._pending_patch_files = []

#endregion: GUI Task Handlers

def _install_sigint_exit_handler(controller: 'AppController') -> None:
 """
  Install a SIGINT handler that drains the controller's I/O pool
  (wait=False) and calls ``os._exit(0)``. This sidesteps the broken
  Python interpreter finalization chain that hangs the process when
  Ctrl+C is pressed while a worker is mid-task in user code
  (e.g. a long-running Gemini/Anthropic HTTP request).
  Background: ``ThreadPoolExecutor.__del__`` -> ``shutdown(wait=True)``
  joins all worker threads; atexit handlers do not fire reliably in
  that scenario, so the interpreter never reaches the pool-shutdown
  path. Bypassing finalization with ``os._exit(0)`` is the only
  reliable fix.
  [SDM: src/app_controller.py:_install_sigint_exit_handler]
  Best-effort: ``signal.signal`` may fail with ``ValueError`` on
  non-main threads (e.g. some conftest warmup paths). The failure
  is swallowed because production (main thread) is the only case
  that matters; the conftest's own atexit fix at commit 8957c9a5
  covers the test fixture's normal-exit path.
  [C: src/app_controller.py:AppController.__init__]
 """
 def _on_sigint(signum: int, frame: Any) -> None:
  try:
   controller._io_pool.shutdown(wait=False)
  except Exception:
   pass
  os._exit(0)
 try:
  signal.signal(signal.SIGINT, _on_sigint)
 except (ValueError, OSError):
  pass


class AppController:
 """
  The headless controller for the Manual Slop application.
  Owns the application state and manages background services.
 """

 def __init__(self, defer_warmup: bool = False, log_to_stderr: Optional[bool] = None):
  """
    [C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
  """
  # --- Startup timeline (startup_speedup_20260606) ---
  # Captured at the very start of __init__ so init_start_ts represents
  # the true cold-start entry point. The other timestamps are filled in
  # lazily as events occur (warmup_done by on_complete callback,
  # first_frame by mark_first_frame_rendered, appcontroller_init_done
  # and gui_run_started by mark_gui_run_started).
  self._init_start_ts:              float = time.time()
  self._warmup_done_ts:             Optional[float] = None
  self._first_frame_ts:             Optional[float] = None
  self._appcontroller_init_done_ts: Optional[float] = None
  self._gui_run_started_ts:         Optional[float] = None
  # --- Locks ---
  self._send_thread_lock:          threading.Lock = threading.Lock()
  self._disc_entries_lock:         threading.Lock = threading.Lock()
  self._pending_comms_lock:        threading.Lock = threading.Lock()
  self._pending_tool_calls_lock:   threading.Lock = threading.Lock()
  self._pending_history_adds_lock: threading.Lock = threading.Lock()
  self._pending_gui_tasks_lock:    threading.Lock = threading.Lock()
  self._pending_dialog_lock:       threading.Lock = threading.Lock()
  self._api_event_queue_lock:      threading.Lock = threading.Lock()
  self._rag_engine_lock:           threading.Lock = threading.Lock()
  self._rag_sync_token:            int = 0
  self._rag_sync_dirty:            bool = False
  self._rag_sync_lock:             threading.Lock = threading.Lock()
  self._project_switch_lock:       threading.Lock = threading.Lock()
  self._project_switch_in_progress: bool = False
  self._project_switch_pending_path:  Optional[str] = None
  self._project_switch_error:         Optional[str] = None
  # --- GUI health state (gui_2.py:618 wrap around immapp.run) ---
  # Set to a non-None string when immapp.run raises a RuntimeError
  # (e.g. IM_ASSERT for an ImGui scope mismatch). The GUI process stays
  # alive (so the hook server can keep serving) but tests can detect the
  # degraded state via /api/gui_health and fail fast.
  self._gui_degraded_reason:         Optional[str] = None
  self._last_imgui_assert:           Optional[str] = None
  # --- Shared background pool + proactive warmup (startup_speedup_20260606) ---
  self._io_pool = make_io_pool()
  _install_sigint_exit_handler(self)
  # Warmup progress is a diagnostic; keep stderr quiet unless explicitly asked.
  # Explicit log_to_stderr arg wins; otherwise default to the SLOP_WARMUP_DEBUG env flag.
  if log_to_stderr is None:
   log_to_stderr = bool(os.environ.get("SLOP_WARMUP_DEBUG"))
  self._warmup = WarmupManager(self._io_pool, log_to_stderr=log_to_stderr)
  # Hook warmup completion to stamp warmup_done_ts for startup_timeline().
  self._warmup.on_complete(self._on_warmup_complete_for_timeline)
  self._warmup_started: bool = False
  self._defer_warmup: bool = defer_warmup
  self._pending_fetch_provider: Optional[str] = None
  # The desktop GUI defers warmup until the first frame is painted (see
  # App._gui_func) so the ~2s of heavy SDK C-extension imports don't hold the
  # GIL while the window and font atlas are being created — that contention is
  # what made the window slow to appear. Headless/web/tests warm immediately.
  if not defer_warmup:
   self.start_warmup()

  #region: --- Internal State ---
  self._ai_status:                        str = "idle"
  self._mma_status:                       str = "idle"
  self._pending_gui_tasks:                List[Dict[str, Any]] = []
  self._api_event_queue:                  List[Dict[str, Any]] = []
  self._pending_dialog:                   Optional[ConfirmDialog] = None
  self._pending_dialog_open:              bool = False
  self._pending_actions:                  Dict[str, ConfirmDialog] = {}
  self._pending_ask_dialog:               bool = False
  self._loop_thread:                      Optional[threading.Thread] = None
  self._worker_status:                    Dict[str, str] = {}
  self._pending_patch_text:               Optional[str] = None
  self._pending_patch_files:              List[str] = []
  self._show_patch_modal:                 bool = False
  self._patch_error_message:              Optional[str] = None
  self._tool_log:                         List[Dict[str, Any]] = []
  self._tool_stats:                       Dict[str, Dict[str, Any]] = {}
  self._cached_cache_stats:               Dict[str, Any] = {}
  self._cached_files:                     List[str] = []
  self._token_history:                    List[Dict[str, Any]] = []
  self._session_start_time:               float = time.time()
  self._ticket_start_times:               dict[str, float] = {}
  self._avg_ticket_time:                  float = 0.0
  self._completed_ticket_count:           int = 0
  self._comms_log:                        List[Dict[str, Any]] = []
  self._last_telemetry_time:              float = 0.0
  self._current_provider:                 str = "gemini"
  self._current_model:                    str = "gemini-2.5-flash-lite"
  self._autofocus_response_tab           = False
  self._show_track_proposal_modal:        bool = False
  self._pending_comms:                    List[Dict[str, Any]] = []
  self._pending_tool_calls:               List[Dict[str, Any]] = []
  self._pending_history_adds:             List[Dict[str, Any]] = []
  self._perf_last_update:                 float = 0.0
  self._last_autosave:                    float = time.time()
  self._ask_dialog_open:                  bool = False
  self._ask_request_id:                   Optional[str] = None
  self._ask_tool_data:                    Optional[Dict[str, Any]] = None
  self._pending_mma_approvals:            List[Dict[str, Any]] = []
  self._mma_approval_open:                bool = False
  self._mma_approval_edit_mode:           bool = False
  self._project_switch_in_progress:       bool = False
  self._project_switch_pending_path:      Optional[str] = None
  self._mma_approval_payload:             str = ""
  self._pending_mma_spawns:               List[Dict[str, Any]] = []
  self._mma_spawn_open:                   bool = False
  self._mma_spawn_edit_mode:              bool = False
  self._mma_spawn_prompt:                 str = ''
  self._mma_spawn_context:                str = ''
  self._trigger_blink:                    bool = False
  self._is_blinking:                      bool = False
  self._blink_start_time:                 float = 0.0
  self._trigger_script_blink:             bool = False
  self._is_script_blinking:               bool = False
  self._script_blink_start_time:          float = 0.0
  self._scroll_disc_to_bottom:            bool = False
  self._scroll_comms_to_bottom:           bool = False
  self._scroll_tool_calls_to_bottom:      bool = False
  self._gemini_cache_text:                str = ""
  self._last_stable_md:                   str = ''
  self._token_stats:                      Dict[str, Any] = {}
  self._comms_log_dirty:                  bool = True
  self._tool_log_dirty:                   bool = True
  self._token_stats_dirty:                bool = True
  self._tier_stream_last_len:             Dict[str, int] = {}
  self._current_session_usage            = None
  self._current_mma_tier_usage           = None
  self.vendor_quota:                      Dict[str, Any] = {}
  self.last_error:                        Optional[Dict[str, str]] = None
  self.token_tracker:                     Dict[str, Any] = {"used": 0, "limit": 0, "cache_hits": 0, "cache_misses": 0}
  self._current_token_history            = None
  self._current_session_start_time       = None
  self._inject_file_path:                 str = ""
  self._inject_mode:                      str = "skeleton"
  self._inject_preview:                   str = ""
  self._show_inject_modal:                bool = False
  self._editing_preset_name:              str = ""
  self._editing_preset_content:           str = ""
  self._editing_preset_temperature:       float = 0.0
  self._editing_preset_top_p:             float = 0.0
  self._editing_preset_max_output_tokens: int = 4096
  self._editing_preset_scope:             str = "project"
  self._editing_tool_preset_name:         str = ""
  self._editing_tool_preset_categories:   Dict[str, Dict[str, Any]] = {}
  self._editing_tool_preset_scope:        str = "project"
  self._show_base_prompt_diff_modal:      bool = False
  self._autosave_interval:                float = 60.0
  self._show_add_ticket_form:             bool = False
  self._track_discussion_active:          bool = False
  #endregion: Internal State

  #region: --- Core State ---
  self.config:                        Dict[str, Any] = {}
  self.project:                       Dict[str, Any] = {}
  self.active_project_path:           str = ""
  self.project_paths:                 List[str] = []
  self.active_discussion:             str = "main"
  self.disc_entries:                  List[Dict[str, Any]] = []
  self.discussion_sent_markdown:      str = ""
  self.discussion_sent_system_prompt: str = ""
  self.disc_roles:                    List[str] = []
  self.tracks:                        List[Dict[str, Any]] = []
  self.active_track:                  Optional[models.Track] = None
  self.engines:                       Dict[str, multi_agent_conductor.ConductorEngine] = {}
  self.mma_streams:                   Dict[str, str] = {}
  self.MAX_STREAM_SIZE:               int = 10 * 1024
  self.session_usage:                 Dict[str, Any] = {
   "input_tokens":                0,
   "output_tokens":               0,
   "cache_read_input_tokens":     0,
   "cache_creation_input_tokens": 0,
   "total_tokens":                0,
   "last_latency":                0.0,
   "percentage":                  0.0
  }
  self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
   "Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None},
   "Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None},
   "Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite",  "tool_preset": None},
   "Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite",  "tool_preset": None},
  }
  self.mcp_config:                      models.MCPConfiguration = models.MCPConfiguration()
  self.view_presets:                    list[models.NamedViewPreset] = []
  self.rag_config:                      Optional[models.RAGConfig] = None
  self.rag_status:                      str = 'idle'
  self.temperature:                     float = 0.0
  self.top_p:                           float = 1.0
  self.max_tokens:                      int = 8192
  self.history_trunc_limit:             int = 8000
  self.ai_response:                     str = ''
  self.last_md:                         str = ''
  self.last_aggregate_markdown:         str = ''
  self.last_resolved_system_prompt:     str = ''
  self.last_md_path:                    Optional[Path] = None
  self.last_file_items:                 List[Any] = []
  self.send_thread:                     Optional[threading.Thread] = None
  self.show_windows:                    Dict[str, bool] = {}
  self.show_script_output:              bool = False
  self.text_viewer_title:               str = ''
  self.text_viewer_content:             str = ''
  self.text_viewer_type:                str = 'text'
  self.perf_history:                    Dict[str, List[float]] = {'frame_time': [0.0]*100, 'fps': [0.0]*100, 'cpu': [0.0]*100, 'input_lag': [0.0]*100}
  self.mma_step_mode:                   bool = False
  self.active_tier:                     Optional[str] = None
  self.test_hooks_enabled:              bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
  self.is_viewing_prior_session:        bool = False
  self.prior_session_entries:           List[Dict[str, Any]] = []
  self.prior_tool_calls:                List[Dict[str, Any]] = []
  self.prior_disc_entries:              List[Dict[str, Any]] = []
  self.prior_mma_dashboard_state       = {}
  self.diagnostic_log:                  List[Dict[str, Any]] = []
  self.ui_active_tool_preset:           str | None = None
  self.ui_active_bias_profile:          str | None = None
  self.available_models:                List[str] = []
  self.all_available_models:            Dict[str, List[str]] = {}
  self.proposed_tracks:                 List[Dict[str, Any]] = []
  self.show_preset_manager_window:      bool = False
  self.show_tool_preset_manager_window: bool = False
  self.show_persona_editor_window:      bool = False
  #endregion: Internal State

  #region: --- UI State ---
  self.ui_ai_input:                  str = ""
  self.ui_disc_new_name_input:       str = ""
  self.ui_disc_new_role_input:       str = ""
  self.ui_epic_input:                str = ""
  self.ui_new_track_name:            str = ""
  self.ui_new_track_desc:            str = ""
  self.ui_new_track_type:            str = "feature"
  self.ui_project_conductor_dir:     str = ""
  self.ui_conductor_setup_summary:   str = ""
  self.ui_last_script_text:          str = ""
  self.ui_last_script_output:        str = ""
  self.ui_new_ticket_id:             str = ""
  self.ui_new_ticket_desc:           str = ""
  self.ui_new_ticket_target:         str = ""
  self.ui_new_ticket_deps:           str = ""
  self.ui_output_dir:                str = ""
  self.ui_files_base_dir:            str = ""
  self.ui_shots_base_dir:            str = ""
  self.ui_project_git_dir:           str = ""
  self.ui_project_system_prompt:     str = ""
  self.ui_project_execution_mode:    str = "native"
  self.ui_gemini_cli_path:           str = "gemini"
  self.ui_word_wrap:                 bool = True
  self.ui_auto_add_history:          bool = False
  self.ui_separate_message_panel:    bool = False
  self.ui_separate_response_panel:   bool = False
  self.ui_separate_tool_calls_panel: bool = False
  self.ui_global_system_prompt:      str = ""
  self.ui_base_system_prompt:        str = ""
  self.ui_use_default_base_prompt:   bool = True
  self.ui_project_context_marker:    str = ""
  self.ui_agent_tools:               Dict[str, bool] = {}
  self.ui_manual_approve:            bool = False
  self.ui_disc_truncate_pairs:       int = 2
  self.ui_auto_scroll_comms:         bool = True
  self.ui_auto_scroll_tool_calls:    bool = True
  self.ui_focus_agent:               Optional[str] = None
  self.ui_active_persona:            str = ""
  #endregion: UI State

  # --- Media/Context ---
  self.files:         List[models.FileItem] = []
  self.context_files: List[models.FileItem] = []
  self.screenshots:   List[str] = []

  # --- Services ---
  self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
  self.rag_engine:  Optional[Any] = None

  # --- Defaults set here so tests that construct AppController without
  # calling init_state() still see the attributes ---
  self.ui_global_preset_name:  Optional[str] = None
  self.active_tickets:          List[Dict[str, Any]] = []
  self.ui_selected_tickets:     Set[str] = set()

  #region: --- Configuration Maps ---
  self._settable_fields: Dict[str, str] = {
   'ai_input':                          'ui_ai_input',
   'project_git_dir':                   'ui_project_git_dir',
   'auto_add_history':                  'ui_auto_add_history',
   'disc_new_name_input':               'ui_disc_new_name_input',
   'gcli_path':                         'ui_gemini_cli_path',
   'output_dir':                        'ui_output_dir',
   'files_base_dir':                    'ui_files_base_dir',
   'files':                             'ui_file_paths',
   'screenshots':                       'screenshots',
   'ai_status':                         'ai_status',
   'ai_response':                       'ai_response',
   'active_discussion':                 'active_discussion',
   'current_provider':                  'current_provider',
   'current_model':                     'current_model',
   'token_budget_pct':                  '_token_budget_pct',
   'token_budget_current':              '_token_budget_current',
   'token_budget_label':                '_token_budget_label',
   'show_confirm_modal':                'show_confirm_modal',
   'mma_epic_input':                    'ui_epic_input',
   'mma_status':                        'mma_status',
   'rag_status':                        'rag_status',
   'rag_enabled':                       'rag_enabled',
   'rag_source':                        'rag_source',
   'rag_emb_provider':                  'rag_emb_provider',
   'rag_mcp_server':                    'rag_mcp_server',
   'rag_mcp_tool':                      'rag_mcp_tool',
   'rag_chunk_size':                    'rag_chunk_size',
   'rag_chunk_overlap':                 'rag_chunk_overlap',
   'rag_collection_name':               'rag_collection_name',
   'mcp_config_json':                   'mcp_config_json',
   'mma_active_tier':                   'active_tier',
   'ui_new_track_name':                 'ui_new_track_name',
   'ui_new_track_desc':                 'ui_new_track_desc',
   'manual_approve':                    'ui_manual_approve',
   'global_system_prompt':              'ui_global_system_prompt',
   'project_system_prompt':             'ui_project_system_prompt',
   'base_system_prompt':                'ui_base_system_prompt',
   'use_default_base_prompt':           'ui_use_default_base_prompt',
   'show_base_prompt_diff_modal':       '_show_base_prompt_diff_modal',
   'global_preset_name':                'ui_global_preset_name',
   'project_preset_name':               'ui_project_preset_name',
   'ui_active_tool_preset':             'ui_active_tool_preset',
   'ui_active_bias_profile':            'ui_active_bias_profile',
   'temperature':                       'temperature',
   'max_tokens':                        'max_tokens',
   'show_preset_manager_window':        'show_preset_manager_window',
   'show_tool_preset_manager_window':   'show_tool_preset_manager_window',
   'show_persona_editor_window':        'show_persona_editor_window',
   '_editing_preset_name':              '_editing_preset_name',
   '_editing_preset_content':           '_editing_preset_content',
   '_editing_preset_temperature':       '_editing_preset_temperature',
   '_editing_preset_top_p':             '_editing_preset_top_p',
   '_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
   '_editing_preset_scope':             '_editing_preset_scope',
   '_editing_tool_preset_name':         '_editing_tool_preset_name',
   '_editing_tool_preset_categories':   '_editing_tool_preset_categories',
   '_editing_tool_preset_scope':        '_editing_tool_preset_scope',
   'show_windows':                      'show_windows',
   'ui_separate_task_dag':              'ui_separate_task_dag',
   'ui_separate_usage_analytics':       'ui_separate_usage_analytics',
   'ui_separate_tier1':                 'ui_separate_tier1',
   'ui_separate_tier2':                 'ui_separate_tier2',
   'ui_separate_tier3':                 'ui_separate_tier3',
   'ui_separate_tier4':                 'ui_separate_tier4',
   'text_viewer_title':                 'text_viewer_title',
   'text_viewer_type':                  'text_viewer_type',
   'disc_entries':                      'disc_entries',
   'ui_file_paths':                     'ui_file_paths',
   'ui_auto_switch_layout':             'ui_auto_switch_layout',
   'ui_tier_layout_bindings':           'ui_tier_layout_bindings',
   'ui_focus_agent':                    'ui_focus_agent'
  }
  self._gettable_fields = dict(self._settable_fields)
  self._gettable_fields.update({
   'show_windows':                      'show_windows', # Key 'show_windows' maps to field 'show_windows' on controller
   'ui_focus_agent':                    'ui_focus_agent',
   'active_discussion':                 'active_discussion',
   '_track_discussion_active':          '_track_discussion_active',
   'proposed_tracks':                   'proposed_tracks',
   'mma_streams':                       'mma_streams',
   '_worker_status':                    '_worker_status',
   'active_track':                      'active_track',
   'active_tickets':                    'active_tickets',
   'tracks':                            'tracks',
   'thinking_indicator':                'thinking_indicator',
   'operations_live_indicator':         'operations_live_indicator',
   'prior_session_indicator':           'prior_session_indicator',
   '_show_patch_modal':                 '_show_patch_modal',
   '_pending_patch_text':               '_pending_patch_text',
   '_pending_patch_files':              '_pending_patch_files',
   '_inject_file_path':                 '_inject_file_path',
   '_inject_mode':                      '_inject_mode',
   '_inject_preview':                   '_inject_preview',
   '_show_inject_modal':                '_show_inject_modal',
   'bg_shader_enabled':                 'bg_shader_enabled',
   'global_system_prompt':              'ui_global_system_prompt',
   'project_system_prompt':             'ui_project_system_prompt',
   'base_system_prompt':                'ui_base_system_prompt',
   'use_default_base_prompt':           'ui_use_default_base_prompt',
   'show_base_prompt_diff_modal':       '_show_base_prompt_diff_modal',
   'global_preset_name':                'ui_global_preset_name',
   'project_preset_name':               'ui_project_preset_name',
   'ui_active_tool_preset':             'ui_active_tool_preset',
   'ui_active_bias_profile':            'ui_active_bias_profile',
   'temperature':                       'temperature',
   'max_tokens':                        'max_tokens',
   'show_preset_manager_window':        'show_preset_manager_window',
   'show_tool_preset_manager_window':   'show_tool_preset_manager_window',
   'show_persona_editor_window':        'show_persona_editor_window',
   '_editing_preset_name':              '_editing_preset_name',
   '_editing_preset_content':           '_editing_preset_content',
   '_editing_preset_temperature':       '_editing_preset_temperature',
   '_editing_preset_top_p':             '_editing_preset_top_p',
   '_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens',
   '_editing_preset_scope':             '_editing_preset_scope',
   'ui_separate_task_dag':              'ui_separate_task_dag',
   'ui_separate_usage_analytics':       'ui_separate_usage_analytics',
   'ui_separate_tier1':                 'ui_separate_tier1',
   'ui_separate_tier2':                 'ui_separate_tier2',
   'ui_separate_tier3':                 'ui_separate_tier3',
   'ui_separate_tier4':                 'ui_separate_tier4',
   'text_viewer_title':                 'text_viewer_title',
   'text_viewer_type':                  'text_viewer_type'
  })
  self.context_preset_manager  = ContextPresetManager()
  self.perf_monitor            = performance_monitor.get_monitor()
  self._perf_profiling_enabled = False
  self._gui_task_handlers: Dict[str, Callable] = {
   "refresh_api_metrics":  _handle_refresh_api_metrics,
   "set_ai_status":        _handle_set_ai_status,
   "set_mma_status":       _handle_set_mma_status,
   "handle_ai_response":   _handle_ai_response,
   "mma_stream":           _handle_mma_stream,
   "mma_stream_append":    _handle_mma_stream,
   "show_track_proposal":  _handle_show_track_proposal,
   "mma_state_update":     _handle_mma_state_update,
   "custom_callback":      _handle_custom_callback,
   "set_value":            _handle_set_value,
   "click":                _handle_click,
   "drag":                 _handle_drag,
   "right_click":          _handle_right_click,
   "select_list_item":     _handle_select_list_item,
   "ask":                  _handle_ask,
   "clear_ask":            _handle_clear_ask,
   "mma_step_approval":    _handle_mma_step_approval,
   "mma_spawn_approval":   _handle_mma_spawn_approval,
   "ticket_started":       _handle_ticket_started,
   "ticket_completed":     _handle_ticket_completed,
   "bead_updated":         _handle_bead_updated,
   "set_comms_dirty":      _handle_set_comms_dirty,
   "set_tool_log_dirty":   _handle_set_tool_log_dirty,
   "refresh_from_project": _handle_refresh_from_project,
   "show_patch_modal":     _handle_show_patch_modal,
   "hide_patch_modal":     _handle_hide_patch_modal,
  }
  #endregion: Configuration Map
  self._init_actions()

 def __getattr__(self, name: str) -> Any:
  """
  Fallback for UI flags (self.ui_*) and config dicts that are
  set in init_state() but not in __init__(). Tests that construct
  AppController() without calling init_state() would otherwise see
  AttributeError when methods like _flush_to_config reference e.g.
  self.ui_separate_task_dag.

  Returns None for missing UI flags (defaulting to "off"). For
  other missing attributes (e.g. 'persona_manager' which is set
  lazily by _load_active_project), raises AttributeError so that
  hasattr() returns False and tests can verify lazy initialization.

  Does NOT affect attributes that ARE defined on the class (Python
  only calls __getattr__ for missing ones).
  """
  # Avoid infinite recursion for dunder/sunder names (e.g. during
  # pickling, copy, etc.) by returning AttributeError.
  if name.startswith("_") or name in (
   "__class__", "__dict__", "__getstate__", "__setstate__",
   "__reduce__", "__reduce_ex__", "__getnewargs__",
  ):
   raise AttributeError(name)
  # Only return a default for the explicit UI flag list (those defined
  # in __init__ as defaults). The previous implementation returned None for
  # ANY ui_ attribute, which broke hasattr() and the App.__setattr__
  # routing logic. For any other missing attribute, raise AttributeError
  # so hasattr() returns False correctly.
  _UI_FLAG_DEFAULTS = {
   "ui_active_tool_preset", "ui_active_bias_profile",
   "ui_ai_input", "ui_disc_new_name_input", "ui_disc_new_role_input",
   "ui_epic_input", "ui_new_track_name", "ui_new_track_desc",
   "ui_new_track_type", "ui_project_conductor_dir",
   "ui_conductor_setup_summary", "ui_last_script_text",
   "ui_last_script_output", "ui_new_ticket_id", "ui_new_ticket_desc",
   "ui_new_ticket_target", "ui_new_ticket_deps", "ui_output_dir",
   "ui_files_base_dir", "ui_shots_base_dir", "ui_project_git_dir",
   "ui_project_system_prompt", "ui_project_execution_mode",
   "ui_gemini_cli_path", "ui_word_wrap", "ui_auto_add_history",
   "ui_separate_message_panel", "ui_separate_response_panel",
   "ui_separate_tool_calls_panel", "ui_global_system_prompt",
   "ui_base_system_prompt", "ui_use_default_base_prompt",
   "ui_project_context_marker", "ui_agent_tools", "ui_manual_approve",
   "ui_disc_truncate_pairs", "ui_auto_scroll_comms",
   "ui_auto_scroll_tool_calls", "ui_focus_agent", "ui_active_persona",
  }
  if name in _UI_FLAG_DEFAULTS or name == "rag_engine":
   return None
  raise AttributeError(name)

 @property
 def init_start_ts(self) -> float:
  """Timestamp when AppController.__init__ started (cold-start entry). [SDM: src/app_controller.py:init_start_ts]"""
  return self._init_start_ts

 @property
 def warmup_done_ts(self) -> "Optional[float]":
  """Timestamp when the warmup completed; None while still running. [SDM: src/app_controller.py:warmup_done_ts]"""
  return self._warmup_done_ts

 @property
 def first_frame_ts(self) -> "Optional[float]":
  """Timestamp of the first GUI frame; None until the App has rendered once. [SDM: src/app_controller.py:first_frame_ts]"""
  return self._first_frame_ts

 def mark_first_frame_rendered(self, ts: "Optional[float]" = None) -> None:
  """Called by the App on the first frame render. Stamps first_frame_ts and logs the timeline to stderr. [SDM: src/app_controller.py:mark_first_frame_rendered] [C: src/gui_2.py:render_main_interface]"""
  if self._first_frame_ts is not None: return
  self._first_frame_ts = ts if ts is not None else time.time()
  try:
   warmup_ms           = (self._warmup_done_ts - self._init_start_ts) * 1000 if self._warmup_done_ts is not None else 0.0
   frame_after_init_ms = (self._first_frame_ts - self._init_start_ts) * 1000
   # Phase breakdown: which main-thread phase dominated?
   init_phase_ms   = (self._appcontroller_init_done_ts - self._init_start_ts)              * 1000 if  self._appcontroller_init_done_ts is not None else None
   gui_phase_ms    = (self._gui_run_started_ts         - self._appcontroller_init_done_ts) * 1000 if (self._gui_run_started_ts         is not None and self._appcontroller_init_done_ts is not None) else None
   render_phase_ms = (self._first_frame_ts             - self._gui_run_started_ts)         * 1000 if  self._gui_run_started_ts         is not None else None
   phase_parts     = []
   if init_phase_ms   is not None: phase_parts.append(f"init={init_phase_ms:.0f}ms")
   if gui_phase_ms    is not None: phase_parts.append(f"gui_setup={gui_phase_ms:.0f}ms")
   if render_phase_ms is not None: phase_parts.append(f"first_render={render_phase_ms:.0f}ms")
   phase_str = f" [{', '.join(phase_parts)}]" if phase_parts else ""
   if self._warmup_done_ts is None:
    gap_str = " (warmup still running at first frame; warmup did NOT block the first frame)"
   else:
    delta_ms = (self._first_frame_ts - self._warmup_done_ts) * 1000
    if delta_ms < 0:
     gap_str = f" (rendered {-delta_ms:.1f}ms BEFORE warmup done \u2014 warmup did NOT block)"
    else:
     gap_str = f" (rendered {delta_ms:.1f}ms AFTER warmup done)"
   sys.stderr.write(f"[startup] first frame at {frame_after_init_ms:.1f}ms after init (warmup took {warmup_ms:.1f}ms){phase_str}{gap_str}\n")
   sys.stderr.flush()
  except Exception: pass

 def mark_gui_run_started(self, ts: "Optional[float]" = None) -> None:
  """Called by App.run() before the heavy imgui bundle setup. Captures
  _appcontroller_init_done_ts (lazily, since this is the first post-init call)
  and _gui_run_started_ts. The 3-phase breakdown in startup_timeline()
  then shows: AppController init, GUI bundle setup, first render.
  [SDM: src/app_controller.py:mark_gui_run_started] [C: src/gui_2.py:App.run]"""
  if self._appcontroller_init_done_ts is None: self._appcontroller_init_done_ts = ts if ts is not None else time.time()
  if self._gui_run_started_ts         is None: self._gui_run_started_ts         = ts if ts is not None else time.time()

 def startup_timeline(self) -> dict:
  """Returns a dict with all startup timestamps and precomputed deltas. Fields: init_start_ts, appcontroller_init_done_ts, gui_run_started_ts, warmup_done_ts, first_frame_ts, warmup_ms, appcontroller_init_ms, gui_setup_ms, first_render_ms, first_frame_after_init_ms, first_frame_after_warmup_ms. The 3 phase breakdowns answer 'which main-thread phase dominated?': AppController init, GUI bundle setup, first render. [SDM: src/app_controller.py:startup_timeline] [C: src/api_hooks.py:HookHandler.do_GET /api/startup_timeline]"""
  result: dict = {
   "cold_start_ts":              self.cold_start_ts,
   "init_start_ts":              self._init_start_ts,
   "appcontroller_init_done_ts": self._appcontroller_init_done_ts,
   "gui_run_started_ts":         self._gui_run_started_ts,
   "warmup_done_ts":             self._warmup_done_ts,
   "first_frame_ts":             self._first_frame_ts,
  }
  if self.cold_start_ts is not None:
   result["module_imports_ms"] = (self._init_start_ts - self.cold_start_ts) * 1000
   if self._first_frame_ts is not None:
    result["cold_start_to_first_frame_ms"] = (self._first_frame_ts - self.cold_start_ts) * 1000
  else:
   result["module_imports_ms"] = None
  if self._warmup_done_ts is not None: 
   result["warmup_ms"] = (self._warmup_done_ts - self._init_start_ts) * 1000
  else:
   result["warmup_ms"] = None
  # 3-phase main-thread breakdown
  if self._appcontroller_init_done_ts is not None:
   result["appcontroller_init_ms"] = (self._appcontroller_init_done_ts - self._init_start_ts) * 1000
  else:
   result["appcontroller_init_ms"] = None
  if self._appcontroller_init_done_ts is not None and self._gui_run_started_ts is not None:
   result["gui_setup_ms"] = (self._gui_run_started_ts - self._appcontroller_init_done_ts) * 1000
  else:
   result["gui_setup_ms"] = None
  if self._gui_run_started_ts is not None and self._first_frame_ts is not None:
   result["first_render_ms"] = (self._first_frame_ts - self._gui_run_started_ts) * 1000
  else:
   result["first_render_ms"] = None
  if self._first_frame_ts is not None:
   result["first_frame_after_init_ms"] = (self._first_frame_ts - self._init_start_ts) * 1000
   if self._warmup_done_ts is not None:
    result["first_frame_after_warmup_ms"] = (self._first_frame_ts - self._warmup_done_ts) * 1000
   else:
    result["first_frame_after_warmup_ms"] = None
  else:
   result["first_frame_after_init_ms"] = None
   result["first_frame_after_warmup_ms"] = None
  return result

 @property
 def cold_start_ts(self) -> "Optional[float]":
  """Timestamp captured at the very first line of sloppy.py (the entry
  point). Used to compute the full 'Python start to first frame' latency,
  which is dominated by module imports. None if the entry point didn't
  expose _SLOPPY_COLD_START_TS. [SDM: src/app_controller.py:cold_start_ts]"""
  try:
   import sloppy as _sloppy
   return getattr(_sloppy, "_SLOPPY_COLD_START_TS", None)
  except (ImportError, AttributeError):
   return None

 def _on_warmup_complete_for_timeline(self, snap: dict) -> None:
  """Callback registered with the WarmupManager. Stamps warmup_done_ts and logs the timeline to stderr. [C: src/app_controller.py:startup_timeline]"""
  self._warmup_done_ts = time.time()
  try:
   warmup_ms = (self._warmup_done_ts - self._init_start_ts) * 1000
   if self._first_frame_ts is None:
    gap_str = f" (first frame not yet rendered at warmup done; warmup took {warmup_ms:.1f}ms)"
   else:
    delta_ms = (self._first_frame_ts - self._warmup_done_ts) * 1000
    if delta_ms < 0:
     gap_str = f" (first frame rendered {-delta_ms:.1f}ms BEFORE warmup done \u2014 warmup did NOT block)"
    else:
     gap_str = f" (first frame rendered {delta_ms:.1f}ms after warmup done)"
   sys.stderr.write(f"[startup] warmup done in {warmup_ms:.1f}ms{gap_str}\n")
   sys.stderr.flush()
  except Exception: pass

 @property
 def perf_profiling_enabled(self) -> bool:
  return self._perf_profiling_enabled

 @perf_profiling_enabled.setter
 def perf_profiling_enabled(self, value: bool) -> None:
  self._perf_profiling_enabled = value
  if hasattr(self, 'perf_monitor'):
   self.perf_monitor.enabled = value

 @property
 def active_project_root(self) -> str:
  if self.active_project_path:
   return str(Path(self.active_project_path).parent)
  return self.ui_files_base_dir

 def _update_inject_preview(self) -> None:
  """
    Updates the preview content based on the selected file and injection mode.
    [C: src/gui_2.py:App._render_text_viewer_window, 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
  target_path = self._inject_file_path
  if not os.path.isabs(target_path):
   target_path = os.path.join(self.active_project_root, target_path)
  if not os.path.exists(target_path):
   self._inject_preview = ""
   return
  try:
   with open(target_path, "r", encoding="utf-8") as f:
    content = f.read()
   if self._inject_mode == "skeleton" and target_path.endswith(".py"):
    parser  = ASTParser("python")
    preview = parser.get_skeleton(content)
   else:
    preview = content
   lines = preview.splitlines()
   if len(lines) > 500:
    preview = "\n".join(lines[:500]) + "\n... (truncated)"
   self._inject_preview = preview
  except Exception as e:
   self._inject_preview = f"Error reading file: {e}"

 @property
 def ai_status(self) -> str:
  return self._ai_status

 @ai_status.setter
 def ai_status(self, value: str) -> None:
  self._ai_status = value

 @property
 def mma_status(self) -> str:
  return self._mma_status

 @mma_status.setter
 def mma_status(self, value: str) -> None:
  self._mma_status = value

 @property
 def thinking_indicator(self) -> bool:
  return self.ai_status in ("sending...", "streaming...")

 @property
 def summary_cache(self) -> Any:
  from src import summarize
  return summarize._summary_cache

 @property
 def rag_enabled(self) -> bool:
  return self.rag_config.enabled if self.rag_config else False

 def _sync_rag_engine(self) -> None:
  """Coalesces multiple rapid setter calls into a single engine rebuild. The token + dirty flag pattern ensures that N setters in quick succession produce ONE sync, not N parallel syncs."""
  with self._rag_sync_lock:
   self._rag_sync_token += 1
   self._rag_sync_dirty = True
   token = self._rag_sync_token
  self.submit_io(lambda: self._do_rag_sync(token))

 def _do_rag_sync(self, token: int) -> None:
  """Worker for the coalesced RAG sync. Loops if new setters arrive mid-sync, returns early if a newer sync supersedes us."""
  while True:
   with self._rag_sync_lock:
    if token != self._rag_sync_token:
     return
    self._rag_sync_dirty = False
   self._set_rag_status("initializing...")
   try:
    from src import rag_engine
    engine = rag_engine.RAGEngine(self.rag_config, self.active_project_root)
    if engine.embedding_provider is None:
     self._set_rag_status("error: RAG embedding provider failed to initialize (e.g. missing dependencies)")
     return
    with self._rag_engine_lock:
     self.rag_engine = engine
    if self.rag_engine and self.rag_engine.is_empty() and self.files:
     self._rebuild_rag_index()
    else:
     self._set_rag_status("ready")
   except Exception as e:
    self._set_rag_status(f"error: {e}")
    sys.stderr.write(f"[DEBUG RAG] Failed to sync engine: {e}\n")
    sys.stderr.flush()
   with self._rag_sync_lock:
    if not self._rag_sync_dirty:
     return
    token = self._rag_sync_token
    self._rag_sync_dirty = False

 @property
 def rag_enabled(self) -> bool:
  return self.rag_config.enabled if self.rag_config else False
 
 @rag_enabled.setter
 def rag_enabled(self, value: bool) -> None:
  if self.rag_config:
   self.rag_config.enabled = value
   self._sync_rag_engine()

 @property
 def rag_source(self) -> str:
  return self.rag_config.vector_store.provider if self.rag_config else 'mock'
 
 @rag_source.setter
 def rag_source(self, value: str) -> None:
  if self.rag_config:
   self.rag_config.vector_store.provider = value
   self._sync_rag_engine()

 @property
 def rag_emb_provider(self) -> str:
  return self.rag_config.embedding_provider if self.rag_config else 'gemini'
 
 @rag_emb_provider.setter
 def rag_emb_provider(self, value: str) -> None:
  if self.rag_config:
   self.rag_config.embedding_provider = value
   self._sync_rag_engine()

 @property
 def rag_chunk_size(self) -> int:
  return self.rag_config.chunk_size if self.rag_config else 1000
 
 @rag_chunk_size.setter
 def rag_chunk_size(self, value: int) -> None:
  if self.rag_config: self.rag_config.chunk_size = value

 @property
 def rag_chunk_overlap(self) -> int:
  return self.rag_config.chunk_overlap if self.rag_config else 200
 @rag_chunk_overlap.setter
 def rag_chunk_overlap(self, value: int) -> None:
  if self.rag_config: self.rag_config.chunk_overlap = value

 @property
 def rag_mcp_server(self) -> str:
  return self.rag_config.vector_store.mcp_server or "" if self.rag_config else ""
 @rag_mcp_server.setter
 def rag_mcp_server(self, value: str) -> None:
  if self.rag_config: self.rag_config.vector_store.mcp_server = value

 @property
 def rag_mcp_tool(self) -> str:
  return self.rag_config.vector_store.mcp_tool or "" if self.rag_config else ""
 @rag_mcp_tool.setter
 def rag_mcp_tool(self, value: str) -> None:
  if self.rag_config: self.rag_config.vector_store.mcp_tool = value

 @property
 def rag_collection_name(self) -> str:
  return self.rag_config.vector_store.collection_name if self.rag_config else "manual_slop"
 @rag_collection_name.setter
 def rag_collection_name(self, value: str) -> None:
  if self.rag_config:
   self.rag_config.vector_store.collection_name = value
   self._sync_rag_engine()

 @property
 def mcp_config_json(self) -> str:
  return json.dumps(self.mcp_config.to_dict()) if self.mcp_config else "{}"
 @mcp_config_json.setter
 def mcp_config_json(self, value: str) -> None:
  try:
   data = json.loads(value)
   self.mcp_config = models.MCPConfiguration.from_dict(data)
  except:
   pass

 @property
 def ui_file_paths(self) -> list[str]:
  return [f.path if hasattr(f, 'path') else str(f) for f in self.files]

 @ui_file_paths.setter
 def ui_file_paths(self, value: list[str]) -> None:
  old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
  new_files = []
  import time
  now = time.time()
  for p in value:
   if p in old_files:
    new_files.append(old_files[p])
   else:
    from src import models
    new_files.append(models.FileItem(path=p, injected_at=now))
  self.files = new_files

 @property
 def operations_live_indicator(self) -> bool:
  return not self.is_viewing_prior_session

 @property
 def prior_session_indicator(self) -> bool:
  return self.is_viewing_prior_session

 def _init_actions(self) -> None:
  # Set up state-related action maps
  self._clickable_actions: dict[str, Callable[..., Any]] = {
   'btn_reset':                 self._handle_reset_session,
   'btn_gen_send':              self._handle_generate_send,
   'btn_md_only':               self._handle_md_only,
   'btn_approve_script':        self._handle_approve_script,
   'btn_reject_script':         self._handle_reject_script,
   'btn_project_save':          self._cb_project_save,
   'btn_disc_create':           self._cb_disc_create,
   'btn_mma_plan_epic':         self._cb_plan_epic,
   'btn_mma_accept_tracks':     self._cb_accept_tracks,
   'btn_mma_start_track':       self._cb_start_track,
   'btn_mma_create_track':      lambda: self._cb_create_track(self.ui_new_track_name, self.ui_new_track_desc, self.ui_new_track_type),
   'btn_approve_tool':          self._handle_approve_ask,
   'btn_approve_mma_step':      lambda: self._handle_mma_respond(approved=True),
   'btn_approve_spawn':         lambda: self._handle_mma_respond(approved=True),
   'btn_prune_logs':            self.cb_prune_logs,
   'btn_reset_base_prompt':     self._cb_reset_base_prompt,
   'btn_show_base_prompt_diff': self._cb_show_base_prompt_diff,
   'btn_rebuild_rag_index':     self._rebuild_rag_index,
   'btn_clear_summary_cache':   self._handle_clear_summary_cache,
  }
  self._drag_actions: dict[str, Callable[..., Any]] = {}
  self._right_clickable_actions: dict[str, Callable[..., Any]] = {}
  self._predefined_callbacks: dict[str, Callable[..., Any]] = {
   '_test_callback_func_write_to_file': self._test_callback_func_write_to_file,
   '_set_env_var':                      lambda k, v: os.environ.update({k: v}),
   '_set_attr':                         lambda k, v: setattr(self, k, v),
   '_apply_preset':                     self._apply_preset,
   '_cb_save_preset':                   self._cb_save_preset,
   '_cb_delete_preset':                 self._cb_delete_preset,
   '_cb_save_tool_preset':              self._cb_save_tool_preset,
   '_cb_delete_tool_preset':            self._cb_delete_tool_preset,
   '_switch_project':                   self._switch_project,
   '_refresh_from_project':             self._refresh_from_project,
   'save_workspace_profile':            self._cb_save_workspace_profile,
   'load_workspace_profile':            self._cb_load_workspace_profile,
   'delete_workspace_profile':          self._cb_delete_workspace_profile,
   '_cb_create_track':                  self._cb_create_track,
  }

 def _update_gcli_adapter(self, path: str) -> None:
  if not ai_client._gemini_cli_adapter:
   ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(path))
  else:
   ai_client._gemini_cli_adapter.binary_path = str(path)

 def _trigger_gui_refresh(self):
  with self._pending_gui_tasks_lock:
   self._pending_gui_tasks.append({'action': 'set_comms_dirty'})
   self._pending_gui_tasks.append({'action': 'set_tool_log_dirty'})

 def _process_pending_gui_tasks(self) -> None:
  """Processes pending GUI tasks from the queue on the main render thread."""
  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:
    self._last_telemetry_time = now
    metrics = self.perf_monitor.get_metrics()
    self.event_queue.websocket_server.broadcast("telemetry", metrics)

  if not self._pending_gui_tasks: return

  with self._pending_gui_tasks_lock:
   tasks = self._pending_gui_tasks[:]
   self._pending_gui_tasks.clear()
  for task in tasks:
   try:
    action = task.get("action") or task.get("type")
    if action:
     session_logger.log_api_hook("PROCESS_TASK", action, str(task))
     if action in self._gui_task_handlers:
      self._gui_task_handlers[action](self, task)
   except Exception as e:
    print(f"Error executing GUI task ({task.get('action') or task.get('type')}): {e}")
    traceback.print_exc()

 def _process_pending_history_adds(self) -> None:
  """
  Synchronizes pending history entries to the active discussion and project state.
  [C: src/gui_2.py:App._render_main_interface]
  """
  with self._pending_history_adds_lock:
   items = self._pending_history_adds[:]
   self._pending_history_adds.clear()
  if not items:
   return
  self._scroll_disc_to_bottom = True
  for item in items:
   item.get("role", "unknown")
   if item.get("role") and item["role"] not in self.disc_roles:
    self.disc_roles.append(item["role"])
   disc_sec    = self.project.get("discussion", {})
   discussions = disc_sec.get("discussions", {})
   disc_data   = discussions.get(self.active_discussion)
   if disc_data is not None:
    if item.get("disc_title", self.active_discussion) == self.active_discussion:
     if self.disc_entries is not disc_data.get("history"):
      if "history" not in disc_data:
       disc_data["history"] = []
      disc_data["history"].append(project_manager.entry_to_str(item))
      disc_data["last_updated"] = project_manager.now_ts()
     with self._disc_entries_lock:
      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.
  [C: src/gui_2.py:App._render_main_interface]
  """
  with self._pending_tool_calls_lock:
   items = self._pending_tool_calls[:]
   self._pending_tool_calls.clear()
  if not items:
   return False
  for item in items:
   self._append_tool_log(
    item.get("script", ""),
    item.get("result", ""),
    source_tier=item.get("source_tier")
   )
  return True

 def _test_callback_func_write_to_file(self, data: str) -> None:
  """A dummy function that a custom_callback would execute for testing."""
  callback_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tests", "artifacts", "temp_callback_output.txt")
  os.makedirs(os.path.dirname(callback_path), exist_ok=True)
  with open(callback_path, "w") as f:
   f.write(data)

 def init_state(self):
  """
  Initializes the application state from configurations.
  [C: src/gui_2.py:App.__init__, src/gui_2.py:App._save_paths, src/gui_2.py:App._set_external_editor_default, 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_context_composition_decoupled.py:test_do_generate_uses_context_files, 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
  self.ui_separate_tier1            = False
  self.ui_separate_tier2            = False
  self.ui_separate_tier3            = False
  self.ui_separate_tier4            = False
  self.ui_separate_message_panel    = False
  self.ui_separate_response_panel   = False
  self.ui_separate_tool_calls_panel = False
  self.ui_separate_external_tools   = False
  self.config = self.load_config()
  theme.load_from_config(self.config)
  ai_cfg = self.config.get("ai", {})
  self._current_provider   = ai_cfg.get("provider", "gemini")
  self._current_model      = ai_cfg.get("model", "gemini-2.5-flash-lite")
  self.temperature         = ai_cfg.get("temperature", 0.0)
  self.top_p               = ai_cfg.get("top_p", 1.0)
  self.max_tokens          = ai_cfg.get("max_tokens", 8192)
  self.history_trunc_limit = ai_cfg.get("history_trunc_limit", 8000)
  projects_cfg = self.config.get("projects", {})
  self.project_paths       = list(projects_cfg.get("paths", []))
  self.active_project_path = projects_cfg.get("active", "")
  self._load_active_project()

  # Track 5: Path isolation overrides
  if self.project and 'paths' in self.project:
   project_root = Path(self.active_project_path).parent if self.active_project_path else Path.cwd()
   proj_paths   = self.project.get('paths', {})
   if 'logs_dir' in proj_paths:
    lpath = Path(proj_paths['logs_dir'])
    if not lpath.is_absolute(): lpath = project_root / lpath
    os.environ['SLOP_LOGS_DIR'] = str(lpath)
   if 'scripts_dir' in proj_paths:
    spath = Path(proj_paths['scripts_dir'])
    if not spath.is_absolute(): spath = project_root / spath
    os.environ['SLOP_SCRIPTS_DIR'] = str(spath)
   paths.reset_resolved()

  path_info           = paths.get_full_path_info()
  self.ui_logs_dir    = str(path_info['logs_dir']['path'])
  self.ui_scripts_dir = str(path_info['scripts_dir']['path'])

  if not self.project or not isinstance(self.project, dict) or "project" not in self.project:
   name = Path(self.active_project_path).stem if self.active_project_path else "unnamed"
   self.project = project_manager.default_project(name)
  self.workspace_manager  = workspace_manager.WorkspaceManager(project_root=Path(self.active_project_path).parent if self.active_project_path else None)
  self.workspace_profiles = self.workspace_manager.load_all_profiles()
  # Deserialize FileItems in files.paths
  raw_paths = self.project.get("files", {}).get("paths", [])
  self.files = []
  for p in raw_paths:
   if isinstance(p, models.FileItem):
    self.files.append(p)
   elif isinstance(p, dict):
    self.files.append(models.FileItem.from_dict(p))
   else:
    self.files.append(models.FileItem(path=str(p)))
  self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
  disc_sec = self.project.get("discussion", {})
  self.disc_roles = list(disc_sec.get("roles", ["User", "AI", "Vendor API", "System", "Reasoning", "Context"]))
  self.active_discussion = disc_sec.get("active", "main")
  disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
  
  with self._disc_entries_lock: self.disc_entries[:] = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
  
  # UI state
  self.ui_output_dir              = self.project.get("output",      {}).get("output_dir", "./md_gen")
  self.ui_files_base_dir          = self.project.get("files",       {}).get("base_dir",   ".")
  self.ui_shots_base_dir          = self.project.get("screenshots", {}).get("base_dir",   ".")
  proj_meta = self.project.get("project", {})
  self.ui_project_git_dir         = proj_meta.get("git_dir", "")
  self.ui_project_conductor_dir   = self.project.get('conductor', {}).get('dir', 'conductor')
  self.ui_project_system_prompt   = proj_meta.get("system_prompt", "")
  self.ui_gemini_cli_path         = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
  self._update_gcli_adapter(self.ui_gemini_cli_path)
  self.ui_word_wrap               = proj_meta.get("word_wrap", True)
  self.ui_auto_add_history        = disc_sec.get("auto_add", False)
  self.ui_global_system_prompt    = self.config.get("ai", {}).get("system_prompt", "")
  self.ui_base_system_prompt      = self.config.get("ai", {}).get("base_system_prompt", "")
  self.ui_use_default_base_prompt = self.config.get("ai", {}).get("use_default_base_prompt", True)
  self.ui_project_context_marker  = proj_meta.get("context_marker", "")
  
  self.preset_manager      = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
  self.presets             = self.preset_manager.load_all()
  self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
  self.tool_presets        = self.tool_preset_manager.load_all_presets()
  self.bias_profiles       = self.tool_preset_manager.load_all_bias_profiles()
  
  mcp_path = self.project.get('project', {}).get('mcp_config_path') or self.config.get('ai', {}).get('mcp_config_path')
  if mcp_path:
   mcp_p = Path(mcp_path)
   if not mcp_p.is_absolute() and self.active_project_path:
    mcp_p = Path(self.active_project_path).parent / mcp_path
   if mcp_p.exists(): self.mcp_config = models.load_mcp_config(str(mcp_p))
   else:              self.mcp_config = models.MCPConfiguration()
  else:
   self.mcp_config = models.MCPConfiguration()
  
  rag_data = self.config.get('rag')
  if rag_data: self.rag_config = models.RAGConfig.from_dict(rag_data)
  else:        self.rag_config = models.RAGConfig()
  
  self.rag_engine = None
  if self.rag_config.enabled: self._sync_rag_engine()
  
  from src.personas import PersonaManager
  self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None)

  from src.vendor_capabilities import get_capabilities
  try:
   caps = get_capabilities(self.current_provider, self.current_model)
  except KeyError:
   caps = None
  if caps is None or caps.model_discovery:
   self._fetch_models(self.current_provider)
  self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET') or ai_cfg.get("active_tool_preset")
  self.ui_active_bias_profile = ai_cfg.get("active_bias_profile")
  ai_client.set_tool_preset(self.ui_active_tool_preset)
  ai_client.set_bias_profile(self.ui_active_bias_profile)
  self.ui_global_preset_name = ai_cfg.get("active_preset")
  self.ui_project_preset_name = proj_meta.get("active_preset")
  
  gui_cfg = self.config.get("gui", {})
  self.ui_separate_message_panel = gui_cfg.get('separate_message_panel', False)
  self.ui_separate_response_panel = gui_cfg.get('separate_response_panel', False)
  self.ui_separate_tool_calls_panel = gui_cfg.get('separate_tool_calls_panel', False)
  self.ui_auto_switch_layout = gui_cfg.get("auto_switch_layout", False)
  self.ui_tier_layout_bindings = gui_cfg.get("tier_layout_bindings", {"Tier 1": "", "Tier 2": "", "Tier 3": "", "Tier 4": ""})
  from src import bg_shader
  bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
  
  _default_windows = {
   "Project Settings":  True,
   "Files & Media":     True,
   "AI Settings":       True,
   "MMA Dashboard":     False,
   "Task DAG":          False,
   "Usage Analytics":   False,
   "Tier 1":            False,
   "Tier 2":            False,
   "Tier 3":            False,
   "Tier 4":            False,
   "Tier 1: Strategy":  True,
   "Tier 2: Tech Lead": True,
   "Tier 3: Workers":   True,
   "Tier 4: QA":        True,
   "Discussion Hub":    True,
   "Operations Hub":    True,
   "Message":           False,
   "Response":          False,
   "Tool Calls":        False,
   "Text Viewer":       False,
   "Theme":             True,
   "Log Management":    True,
   "Diagnostics":       True,
  }
  saved = self.config.get("gui", {}).get("show_windows", {})
  self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()}
  agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
  self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
  label = self.project.get("project", {}).get("name", "")
  session_logger.reset_session(label=label)
  # Trigger auto-start of MCP servers
  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():
   if cfg.auto_start:
    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, src/gui_2.py:App.cb_load_prior_log]
  """
  if not path:
   return

  if not self.is_viewing_prior_session:
   self._current_session_usage = copy.deepcopy(self.session_usage)
   self._current_mma_tier_usage = copy.deepcopy(self.mma_tier_usage)
   self._current_token_history = copy.deepcopy(self._token_history)
   self._current_session_start_time = self._session_start_time

  log_path = Path(path)
  if log_path.is_dir():
   log_file = log_path / "comms.log"
   session_dir = log_path
  else:
   log_file = log_path
   session_dir = log_path.parent

  if not log_file.exists():
   self.ai_status = f"log file not found: {log_file}"
   return

  def _resolve_log_ref(content: Any, session_dir: Path) -> str:
   if not content or not isinstance(content, str) or "[REF:" not in content:
    return str(content) if content is not None else ""
   pattern = r'\[REF:([^\]]+)\]'
   def replace_ref(match):
    ref_file = match.group(1)
    paths_to_check = [
     session_dir / "outputs" / ref_file,
     session_dir / "scripts" / ref_file
    ]
    for p in paths_to_check:
     if p.exists():
      try:
       with open(p, "r", encoding="utf-8") as rf:
        return rf.read()
      except Exception:
       return f"[ERROR READING REF: {ref_file}]"
    return match.group(0)
   return re.sub(pattern, replace_ref, content)

  entries = []
  disc_entries = []
  paired_tools = {}
  final_tool_calls = []
  new_token_history = []
  new_usage = {'input_tokens': 0, 'output_tokens': 0, 'cache_read_input_tokens': 0, 'cache_creation_input_tokens': 0, 'total_tokens': 0, 'last_latency': 0.0, 'percentage': 0.0}
  new_mma_usage = copy.deepcopy(self.mma_tier_usage)
  for t in new_mma_usage:
   new_mma_usage[t]['input'] = 0
   new_mma_usage[t]['output'] = 0

  try:
   with open(log_file, "r", encoding="utf-8") as f:
    for line in f:
     line = line.strip()
     if line:
      try:
       entry = json.loads(line)
       entries.append(entry)
       kind = entry.get("kind", entry.get("type", ""))
       payload = entry.get("payload", {})
       ts = entry.get("ts", "")

       if kind == 'tool_call':
        tid = payload.get('id') or payload.get('call_id')
        script = payload.get('script') or json.dumps(payload.get('args', {}), indent=1)
        script = _resolve_log_ref(script, session_dir)
        entry_obj = {
         'source_tier': entry.get('source_tier', 'main'),
         'script': script,
         'result': '', # Waiting for result
         'ts': ts
        }
        if tid:
         paired_tools[tid] = entry_obj
        final_tool_calls.append(entry_obj)
       elif kind == 'tool_result':
        tid = payload.get('id') or payload.get('call_id')
        output = payload.get('output', payload.get('content', ''))
        output = _resolve_log_ref(output, session_dir)
        if tid and tid in paired_tools:
         paired_tools[tid]['result'] = output
        else:
         # Fallback: if no ID, try matching last entry in final_tool_calls that has no result
         for old_call in reversed(final_tool_calls):
          if not old_call['result']:
           old_call['result'] = output
           break
       
       if kind == 'response' and 'usage' in payload:
        u = payload['usage']
        for k in ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'total_tokens']:
         if k in new_usage: new_usage[k] += u.get(k, 0) or 0
        tier = entry.get('source_tier', 'main')
        if tier in new_mma_usage:
         new_mma_usage[tier]['input'] += u.get('input_tokens', 0) or 0
         new_mma_usage[tier]['output'] += u.get('output_tokens', 0) or 0
        new_token_history.append({
         'time': ts,
         'input': u.get('input_tokens', 0) or 0,
         'output': u.get('output_tokens', 0) or 0,
         'model': entry.get('model', 'unknown')
        })
       
       if kind == "history_add":
        content = payload.get("content", payload.get("text", payload.get("message", "")))
        content = _resolve_log_ref(content, session_dir)
        disc_entries.append({
         "role": payload.get("role", "AI"),
         "content": content,
         "collapsed": payload.get("collapsed", False),
         "ts": ts
        })
       elif kind == "request":
        content = payload.get("message", payload.get("content", payload.get("text", "")))
        content = _resolve_log_ref(content, session_dir)
        disc_entries.append({
         "role": "User",
         "content": content,
         "collapsed": False,
         "ts": ts
        })
       elif kind == "response":
        text = payload.get("text", payload.get("content", payload.get("message", "")))
        text = _resolve_log_ref(text, session_dir)
        tool_calls = payload.get("tool_calls", [])
        content = text
        if tool_calls:
         try:
          tc_str = json.dumps(tool_calls, indent=1)
          if content:
           content += f"\n\n[TOOL CALLS]\n{tc_str}"
          else:
           content = f"[TOOL CALLS]\n{tc_str}"
         except:
          if content:
           content += f"\n\n[TOOL CALLS PRESENT]"
          else:
           content = "[TOOL CALLS PRESENT]"
        disc_entries.append({
         "role": "AI",
         "content": content,
         "collapsed": False,
         "ts": ts
        })
       elif kind == "tool_result":
        output = payload.get("output", payload.get("content", ""))
        output = _resolve_log_ref(output, session_dir)
        disc_entries.append({
         "role": "Tool",
         "content": f"[TOOL RESULT]\n{output}",
         "collapsed": True,
         "ts": ts
        })
      except json.JSONDecodeError:
       continue
  except Exception as e:
   self.ai_status = f"log load error: {e}"
   return

  self.session_usage = new_usage
  self.mma_tier_usage = new_mma_usage
  self._token_history = new_token_history
  if new_token_history:
   try:
    import datetime
    first_ts = new_token_history[0]['time']
    dt = datetime.datetime.strptime(first_ts, '%Y-%m-%dT%H:%M:%S')
    self._session_start_time = dt.timestamp()
   except:
    self._session_start_time = time.time()
  self.prior_session_entries = entries
  self.prior_disc_entries = disc_entries
  self.prior_tool_calls = final_tool_calls
  self.is_viewing_prior_session = True
  self._trigger_gui_refresh()
  self.ai_status = f"viewing prior session: {session_dir.name} ({len(entries)} entries)"

 def cb_exit_prior_session(self):
  """
    [C: src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._render_prior_session_view]
  """
  self.is_viewing_prior_session = False
  if self._current_session_usage:
   self.session_usage = self._current_session_usage
   self._current_session_usage = None
  if self._current_mma_tier_usage:
   self.mma_tier_usage = self._current_mma_tier_usage
   self._current_mma_tier_usage = None

  if self._current_token_history is not None:
   self._token_history = self._current_token_history
   self._current_token_history = None
  if self._current_session_start_time is not None:
   self._session_start_time = self._current_session_start_time
   self._current_session_start_time = None

  self.prior_session_entries.clear()
  self.prior_disc_entries.clear()
  self.prior_tool_calls.clear()
  self._trigger_gui_refresh()
  self.ai_status = 'idle'

 def cb_prune_logs(self) -> None:
  """Manually triggers the log pruning process with aggressive thresholds."""
  self.ai_status = "Manual prune started (Age > 0d, Size < 100KB)..."

  def run_manual_prune() -> None:
   try:
    from src import log_registry
    from src import log_pruner
    registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
    pruner = log_pruner.LogPruner(registry, str(paths.get_logs_dir()))
    # Aggressive: Prune anything not whitelisted, even if just created, if under 100KB
    # Note: max_age_days=0 means cutoff is NOW.
    pruner.prune(max_age_days=0, min_size_kb=100)
    self.ai_status = "Manual prune complete."
   except Exception as e:
    self.ai_status = f"Manual prune error: {e}"
    print(f"Error during manual log pruning: {e}")

  self.submit_io(run_manual_prune)

 def _load_active_project(self) -> None:
  """Loads the active project configuration, with fallbacks."""
  if self.active_project_path and Path(self.active_project_path).exists():
   try:
    self.project = project_manager.load_project(self.active_project_path)
   except Exception as e:
    print(f"Failed to load project {self.active_project_path}: {e}")
    self.project = project_manager.migrate_from_legacy_config(self.config)
    self.active_project_path = ""
  else:
   self.project = project_manager.migrate_from_legacy_config(self.config)
   self.active_project_path = ""
  if not self.active_project_path:
   for pp in self.project_paths:
    if Path(pp).exists():
     try:
      self.project = project_manager.load_project(pp)
      self.active_project_path = pp
      break
     except Exception:
      continue
  if not self.active_project_path:
   name = self.project.get("project", {}).get("name", "project")
   fallback_path = f"{name}.toml"
   project_manager.save_project(self.project, fallback_path)
   self.active_project_path = fallback_path
   if fallback_path not in self.project_paths:
    self.project_paths.append(fallback_path)
  self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
  self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None)
  from src.personas import PersonaManager
  self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None)
  self._refresh_from_project()
  self._configure_mcp_for_project()

 def inject_context(self, data: dict) -> None:
  """
  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):
    file_path = os.path.relpath(file_path, self.active_project_root)
   existing = next((f for f in self.files if (f.path if hasattr(f, "path") else str(f)) == file_path), None)
   if not existing:
    item = models.FileItem(path=file_path)
    self.files.append(item)
    self._refresh_from_project()

 def _prune_old_logs(self) -> None:
  """Asynchronously prunes old insignificant logs on startup."""

  def run_prune() -> None:
   try:
    from src import log_registry
    from src import log_pruner
    registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml"))
    pruner = log_pruner.LogPruner(registry, str(paths.get_logs_dir()))
    pruner.prune()
   except Exception as e:
    print(f"Error during log pruning: {e}")
  self.submit_io(run_prune)

 def start_services(self, app: Any = None):
  """
  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 _compute_warmup_list(self) -> list[str]:
  """
  Returns the list of modules to warm on the _io_pool at startup.
  [SDM: src/app_controller.py:_compute_warmup_list]
  """
  modules: list[str] = [
   "google.genai",
   "anthropic",
   "openai",
   "requests",
   "src.command_palette",
   "src.theme_nerv",
   "src.theme_nerv_fx",
   "src.markdown_table",
   "numpy",
  ]
  if getattr(self, "test_hooks_enabled", False):
   modules.extend([
    "fastapi",
    "fastapi.security.api_key",
   ])
  return modules

 def warmup_status(self) -> dict:
  """
 Snapshot of the warmup progress. {pending, completed, failed}.
 Cheap (lock-guarded copy). Polled by the GUI status indicator.
 [SDM: src/app_controller.py:warmup_status]
  """
  return self._warmup.status()

 def warmup_canaries(self) -> list[dict]:
  """
   Per-module import canary records. Each record carries: canary_id, module, thread_name, thread_id, submit_ts, start_ts, end_ts, elapsed_ms, status, error. Useful for debugging which worker thread loaded which module and how long it took. Returns a defensive copy (caller mutation is safe). [SDM: src/app_controller.py:warmup_canaries].
  """
  return self._warmup.canaries()

 def is_warmup_done(self) -> bool:
  """
   True once all warmup jobs have completed (or failed).
   [SDM: src/app_controller.py:is_warmup_done]
  """
  return self._warmup.is_done()

 def start_warmup(self) -> None:
  """
  Submit the heavy-module warmup jobs to the io_pool (idempotent).

  Separated from __init__ so the desktop GUI can call it AFTER the first
  frame is painted, keeping the ~2s of SDK C-extension imports off the GIL
  while the window is being created. Safe to call multiple times.
  """
  if self._warmup_started:
   return
  self._warmup_started = True
  self._warmup.submit(self._compute_warmup_list())
  # Run any model fetch that was deferred while the window was being created.
  if self._pending_fetch_provider is not None:
   provider, self._pending_fetch_provider = self._pending_fetch_provider, None
   self._fetch_models(provider)

 def wait_for_warmup(self, timeout: Optional[float] = None) -> bool:
  """
   Block until warmup completes. Returns True on done, False on timeout.
   [SDM: src/app_controller.py:wait_for_warmup]
  """
  return self._warmup.wait(timeout=timeout)

 def on_warmup_complete(self, callback: Callable[[dict], None]) -> None:
  """
   Register a callback for warmup completion. If already done, fires
   immediately on the calling thread.
   [SDM: src/app_controller.py:on_warmup_complete]
  """
  self._warmup.on_complete(callback)

 def submit_io(self, fn: Callable, *args: Any, **kwargs: Any) -> Any:
  """
  Submit a background job to the shared _io_pool. Use this for any
  fire-and-forget background work; avoids the per-spawn cost of a new
  threading.Thread.

  Returns a concurrent.futures.Future that can be used to track
  completion, raise exceptions, or cancel the job. The pool is capped
  at 4 workers (see src/io_pool.py) so the job may queue briefly if
  the pool is saturated.

  The number of in-flight (running or queued) jobs is tracked via
  self._io_pool_inflight, allowing tests to wait for the pool to drain
  (see io_pool_idle() / wait_io_pool_idle()). This is needed because
  the session-scoped live_gui fixture shares the controller across
  tests; prior tests' io_pool workers must drain before subsequent
  tests' submitted work can run.

  Domain-specific threads (HookServer, WebSocketServer, MMA WorkerPool,
  asyncio loop) are NOT submitted here - they have their own lifecycle
  management.
  [SDM: src/app_controller.py:submit_io]
  """
  if not hasattr(self, "_io_pool_inflight_lock"):
   self._io_pool_inflight_lock = threading.Lock()
  with self._io_pool_inflight_lock:
   self._io_pool_inflight = getattr(self, "_io_pool_inflight", 0) + 1
  future = self._io_pool.submit(fn, *args, **kwargs)
  future.add_done_callback(lambda _f: self._io_pool_inflight_done())
  return future

 def _io_pool_inflight_done(self) -> None:
  """Decrement the in-flight io_pool counter. Called by future callback."""
  with self._io_pool_inflight_lock:
   if getattr(self, "_io_pool_inflight", 0) > 0:
    self._io_pool_inflight -= 1

 def io_pool_idle(self) -> bool:
  """True if no io_pool jobs are currently in-flight (running or queued).

  Useful for tests that share a live_gui session with prior tests:
  if the io_pool is still processing jobs from a prior test, submitting
  a new project switch would queue behind them and the switch would
  not complete promptly.
  [C: tests/test_live_workflow.py:test_full_live_workflow]
  """
  return getattr(self, "_io_pool_inflight", 0) == 0

 def wait_io_pool_idle(self, timeout: float = 60.0, poll_interval: float = 0.1) -> bool:
  """Blocks until io_pool_idle() is True or timeout. Returns True on idle."""
  start = time.time()
  while time.time() - start < timeout:
   if self.io_pool_idle():
    return True
   time.sleep(poll_interval)
  return False

 def shutdown(self) -> None:
  """
  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:
   self.hook_server.stop()
  self.event_queue.put("shutdown", None)
  if self._loop_thread and self._loop_thread.is_alive():
   self._loop_thread.join(timeout=2.0)
  if hasattr(self, "_io_pool") and self._io_pool is not None:
   self._io_pool.shutdown(wait=False)

 def _init_ai_and_hooks(self, app: Any = None) -> None:
  from src import api_hooks
  # validate=False: skip the live model-list lookup (network/subprocess +
  # provider-SDK import) on the main thread during startup. _fetch_models
  # corrects the model against the live list after the first frame, off-thread.
  ai_client.set_provider(self._current_provider, self._current_model, validate=False)
  if self._current_provider == "gemini_cli":
   if not ai_client._gemini_cli_adapter:
    ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=self.ui_gemini_cli_path)
   else:
    ai_client._gemini_cli_adapter.binary_path = self.ui_gemini_cli_path
  ai_client.confirm_and_run_callback = self._confirm_and_run
  ai_client.set_comms_log_callback(self._on_comms_entry)
  ai_client.tool_log_callback = self._on_tool_log
  mcp_client.perf_monitor_callback = self.perf_monitor.get_metrics
  self.perf_monitor.alert_callback = self._on_performance_alert
  ai_client.events.on("request_start", lambda **kw: self._on_api_event("request_start", **kw))
  ai_client.events.on("response_received", lambda **kw: self._on_api_event("response_received", **kw))
  ai_client.events.on("tool_execution", lambda **kw: self._on_api_event("tool_execution", **kw))
  self.hook_server = api_hooks.HookServer(app if app else self)
  self.hook_server.start()

 def _run_event_loop(self):
  """Internal loop runner."""

  def queue_fallback() -> None:
   while True:
    try:
     if hasattr(self, '_process_pending_gui_tasks'):
      self._process_pending_gui_tasks()
     if hasattr(self, '_process_pending_history_adds'):
      self._process_pending_history_adds()
    except: pass
    time.sleep(0.1)
  self.submit_io(queue_fallback)
  self._process_event_queue()

 @property
 def _pending_mma_spawn(self) -> Optional[Dict[str, Any]]:
  return self._pending_mma_spawns[0] if self._pending_mma_spawns else None

 @property
 def _pending_mma_approval(self) -> Optional[Dict[str, Any]]:
  return self._pending_mma_approvals[0] if self._pending_mma_approvals else None

 @property
 def current_provider(self) -> str:
  return self._current_provider

 @current_provider.setter
 def current_provider(self, value: str) -> None:
  if value != self._current_provider:
   self._current_provider = value
   ai_client.reset_session()
   ai_client.set_provider(value, self.current_model)
   self.available_models = self.all_available_models.get(value, [])
   if not self.available_models:
    self._fetch_models(value)
   self._token_stats = {}
   self._token_stats_dirty = True

 @property
 def current_model(self) -> str:
  return self._current_model

 @current_model.setter
 def current_model(self, value: str) -> None:
  if value != self._current_model:
   self._current_model = value
   ai_client.reset_session()
   ai_client.set_provider(self.current_provider, value)
   self._token_stats = {}
   self._token_stats_dirty = True

 def create_api(self) -> FastAPI:
  """
 Creates and configures the FastAPI application for headless mode.
 [SDM: src/app_controller.py:AppController.create_api]
 [C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp]
  """
  fastapi = _require_warmed("fastapi")
  FastAPI = fastapi.FastAPI
  Depends = fastapi.Depends
  HTTPException = fastapi.HTTPException
  api_key_mod = _require_warmed("fastapi.security.api_key")
  APIKeyHeader = api_key_mod.APIKeyHeader
  api = FastAPI(title="Manual Slop Headless API")
  API_KEY_NAME = "X-API-KEY"
  api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
  async def get_api_key(header_key: str = Depends(api_key_header)) -> str:
   return await _api_get_key(self, header_key)
  @api.get("/health")
  def health() -> dict[str, str]:
   return _api_health(self)
  @api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
  def get_gui_state() -> dict[str, Any]:
   """
   [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, tests/test_visual_mma.py:test_visual_mma_components]
   """
   return _api_get_gui_state(self)
  @api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)])
  def get_mma_status() -> dict[str, Any]:
   """
   [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_mma.py:test_visual_mma_components, 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 _api_get_mma_status(self)
  @api.post("/api/gui", dependencies=[Depends(get_api_key)])
  def post_gui(req: dict) -> dict[str, str]:
   """
   [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]
   """
   return _api_post_gui(self, req)
  @api.get("/api/session", dependencies=[Depends(get_api_key)])
  def get_api_session() -> dict[str, Any]:
   return _api_get_api_session(self)
  @api.post("/api/session", dependencies=[Depends(get_api_key)])
  def post_api_session(req: dict) -> dict[str, str]:
   return _api_post_api_session(self, req)
  @api.get("/api/project", dependencies=[Depends(get_api_key)])
  def get_api_project() -> dict[str, Any]:
   return _api_get_api_project(self)
  @api.get("/api/performance", dependencies=[Depends(get_api_key)])
  def get_performance() -> dict[str, Any]:
   """
   [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]
   """
   return _api_get_performance(self)
  @api.get("/api/gui/diagnostics", dependencies=[Depends(get_api_key)])
  def get_diagnostics() -> dict[str, Any]:
   return _api_get_diagnostics(self)
  @api.get("/status", dependencies=[Depends(get_api_key)])
  def status() -> dict[str, Any]:
   return _api_status(self)
  @api.post("/api/v1/generate", dependencies=[Depends(get_api_key)])
  def generate(req: GenerateRequest) -> dict[str, Any]:
   return _api_generate(self, req)
  @api.post("/api/v1/stream", dependencies=[Depends(get_api_key)])
  async def stream(req: GenerateRequest) -> Any:
   return await _api_stream(self, req)
  @api.get("/api/v1/pending_actions", dependencies=[Depends(get_api_key)])
  def pending_actions() -> list[dict[str, Any]]:
   return _api_pending_actions(self)
  @api.post("/api/v1/confirm/{action_id}", dependencies=[Depends(get_api_key)])
  def confirm_action(action_id: str, req: ConfirmRequest) -> dict[str, str]:
   return _api_confirm_action(self, action_id, req)
  @api.get("/api/v1/sessions", dependencies=[Depends(get_api_key)])
  def list_sessions() -> list[str]:
   return _api_list_sessions(self)
  @api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
  def get_session(session_id: str) -> dict[str, Any]:
   """
   [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]
   """
   return _api_get_session(self, session_id)
  @api.delete("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
  def delete_session(session_id: str) -> dict[str, str]:
   return _api_delete_session(self, session_id)
  @api.get("/api/v1/context", dependencies=[Depends(get_api_key)])
  def get_context() -> dict[str, Any]:
   """
   [C: src/fuzzy_anchor.py:FuzzyAnchor.create_slice]
   """
   return _api_get_context(self)
  @api.get("/api/v1/token_stats", dependencies=[Depends(get_api_key)])
  def token_stats() -> dict[str, Any]:
   return _api_token_stats(self)
  return api

 def clear_last_error(self) -> None:
  """Reset last_error after a successful response cycle.
  [C: src/vendor_state.py:get_vendor_state]
  """
  self.last_error = None

 #region: Layout

 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)
  self.workspace_manager.save_profile(profile, scope=scope)
  self.workspace_profiles = self.workspace_manager.load_all_profiles()
  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)

 #endregion: Layout

 #region: Serialization

 def _flush_to_project(self) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_entry_controls, src/gui_2.py:App._render_main_interface, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._show_menus, tests/test_view_presets.py:test_save_view_preset]
  """
  proj = self.project
  proj.setdefault("output", {})["output_dir"] = self.ui_output_dir
  proj.setdefault("files", {})["base_dir"] = self.ui_files_base_dir
  proj["files"]["paths"] = self.files
  proj.setdefault("screenshots", {})["base_dir"] = self.ui_shots_base_dir
  proj["screenshots"]["paths"] = self.screenshots
  proj.setdefault("project", {})
  proj["project"]["git_dir"] = self.ui_project_git_dir
  proj.setdefault("conductor", {})["dir"] = self.ui_project_conductor_dir
  proj["project"]["system_prompt"] = self.ui_project_system_prompt
  proj["project"]["active_preset"] = self.ui_project_preset_name
  proj["project"]["word_wrap"] = self.ui_word_wrap
  proj["project"]["auto_scroll_comms"] = self.ui_auto_scroll_comms
  proj["project"]["auto_scroll_tool_calls"] = self.ui_auto_scroll_tool_calls
  proj.setdefault("gemini_cli", {})["binary_path"] = self.ui_gemini_cli_path
  proj.setdefault("agent", {}).setdefault("tools", {})
  for t_name in models.AGENT_TOOL_NAMES:
   proj["agent"]["tools"][t_name] = self.ui_agent_tools.get(t_name, True)
  self._flush_disc_entries_to_project()
  disc_sec = proj.setdefault("discussion", {})
  disc_sec["roles"] = self.disc_roles
  disc_sec["active"] = self.active_discussion
  disc_sec["auto_add"] = self.ui_auto_add_history
  proj["view_presets"] = [vp.to_dict() for vp in self.view_presets]
  # Save MMA State
  mma_sec = proj.setdefault("mma", {})
  mma_sec["epic"] = self.ui_epic_input
  mma_sec["tier_models"] = {t: {"model": d.get("model"), "provider": d.get("provider", "gemini"), "tool_preset": d.get("tool_preset")} for t, d in self.mma_tier_usage.items()}
  if self.active_track:
   mma_sec["active_track"] = asdict(self.active_track)
  else:
   mma_sec["active_track"] = None

  cleaned_proj = project_manager.clean_nones(proj)
  project_manager.save_project(cleaned_proj, self.active_project_path)

 def _flush_to_config(self) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_entry_controls, src/gui_2.py:App._render_main_interface, 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,
   "temperature": self.temperature,
   "top_p": self.top_p,
   "max_tokens": self.max_tokens,
   "history_trunc_limit": self.history_trunc_limit,
   "active_preset": self.ui_global_preset_name,
  }
  self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
  self.config["ai"]["base_system_prompt"] = self.ui_base_system_prompt
  self.config["ai"]["use_default_base_prompt"] = self.ui_use_default_base_prompt

  if self.rag_config:
   self.config["rag"] = self.rag_config.to_dict()

  self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
  from src import bg_shader
  # Update gui section while preserving other keys like bg_shader_enabled
  gui_cfg = self.config.get("gui", {})
  gui_cfg.update({
   "show_windows": self.show_windows,
   "separate_message_panel": getattr(self, "ui_separate_message_panel", False),
   "separate_response_panel": getattr(self, "ui_separate_response_panel", False),
   "separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False),
   "separate_external_tools": getattr(self, "ui_separate_external_tools", False),
   "separate_task_dag": self.ui_separate_task_dag,
   "separate_usage_analytics": self.ui_separate_usage_analytics,
   "separate_tier1": self.ui_separate_tier1,
   "separate_tier2": self.ui_separate_tier2,
   "separate_tier3": self.ui_separate_tier3,
   "separate_tier4": self.ui_separate_tier4,
   "bg_shader_enabled": bg_shader.get_bg().enabled
  })
  self.config["gui"] = gui_cfg

  # Explicitly save theme state into the config dict
  theme.save_to_config(self.config)

 #endregion: Serialization

 #region: Diagnostics
 
 def _on_performance_alert(self, message: str) -> None:
  self.diagnostic_log.append({
   "ts": project_manager.now_ts(),
   "message": message,
   "type": "performance"
   })
 
 #endregion: Diagnostics

 #region: Usage Analytics

 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)
  total_tokens = total_input + total_output
  elapsed_min = (time.time() - self._session_start_time) / 60.0 if self._token_history else 0
  burn_rate = total_tokens / elapsed_min if elapsed_min > 0 else 0
  session_cost = cost_tracker.estimate_cost("gemini-2.5-flash", total_input, total_output)
  completed = sum(1 for t in self.active_tickets if t.get("status") == "complete")
  efficiency = total_tokens / completed if completed > 0 else 0
  return {
   "total_tokens": total_tokens,
   "total_input": total_input,
   "total_output": total_output,
   "elapsed_min": elapsed_min,
   "burn_rate": burn_rate,
   "session_cost": session_cost,
   "completed_tickets": completed,
   "efficiency": efficiency,
   "call_count": len(self._token_history)
  }

 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"]:
   self.session_usage["percentage"] = payload["usage"]["percentage"]
  self._recalculate_session_usage()
  if md_content is not None:
   stats = ai_client.get_token_stats(md_content)
   # Ensure compatibility if keys are named differently
   if "total_tokens" in stats and "estimated_prompt_tokens" not in stats:
    stats["estimated_prompt_tokens"] = stats["total_tokens"]
   self._token_stats = stats
  cache_stats = payload.get("cache_stats")
  if cache_stats:
   count = cache_stats.get("cache_count", 0)
   size_bytes = cache_stats.get("total_size_bytes", 0)
   self._gemini_cache_text = f"Gemini Caches: {count} ({size_bytes / 1024:.1f} KB)"
  quota = payload.get("vendor_quota")
  if isinstance(quota, dict) and quota:
   self.vendor_quota = quota
  if "error" in payload and isinstance(payload["error"], dict):
   self.last_error = payload["error"]
  self._update_cached_stats()

 def set_vendor_quota(self, provider: str, remaining_pct: float, reset_at: str = "") -> None:
  """Update vendor quota state from a quota-bearing API response.
  [C: src/vendor_state.py:get_vendor_state]
  """
  self.vendor_quota = {"provider": provider, "remaining_pct": remaining_pct, "reset_at": reset_at}

 def _recalculate_session_usage(self) -> None:
  usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "total_tokens": 0, "last_latency": 0.0, "percentage": self.session_usage.get("percentage", 0.0)}
  for entry in ai_client.get_comms_log():
   if entry.get("kind") == "response" and "usage" in entry.get("payload", {}):
    u = entry["payload"]["usage"]
    for k in ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"]:
     if k in usage:
      usage[k] += u.get(k, 0) or 0
  self.session_usage = usage
  # Update cached files list
  stats = ai_client.get_gemini_cache_stats()
  self._cached_files = stats.get("cached_files", [])

 #endregion: Usage Analytics

 #region: Project

 def _configure_mcp_for_project(self) -> None:
  if not self.active_project_path:
   return
  project_root = Path(self.active_project_path).parent
  file_items_as_dicts = [{"path": f.path if hasattr(f, "path") else str(f)} for f in self.files]
  mcp_client.configure(file_items_as_dicts, [str(project_root)])

 def _cb_new_project_automated(self, user_data: Any) -> None:
  if user_data:
   name = Path(user_data).stem
   proj = project_manager.default_project(name)
   project_manager.save_project(proj, user_data)
   if user_data not in self.project_paths:
    self.project_paths.append(user_data)
   self._switch_project(user_data)

 def _cb_project_save(self) -> None:
  self._flush_to_project()
  self._flush_to_config()
  self.save_config()
  self.ai_status = "config saved"

 def _do_project_switch(self, path: str) -> None:
  self._project_switch_error = None
  try:
   self._flush_to_project()
   try:
    new_project = project_manager.load_project(path)
   except Exception as e:
    self.ai_status = f"failed to load project: {e}"
    self._project_switch_error = f"load failed: {e}"
    return
   try:
    self.project = new_project
    self.active_project_path = path
    new_root = Path(path).parent
    self.preset_manager = presets.PresetManager(new_root)
    self.tool_preset_manager = tool_presets.ToolPresetManager(new_root)
    from src.personas import PersonaManager
    self.persona_manager = PersonaManager(new_root)
   except Exception as e:
    self.ai_status = f"failed to init managers: {e}"
    self._project_switch_error = f"manager init failed: {e}"
    return
   self._refresh_from_project()
   file_items_as_dicts = [{"path": f.path if hasattr(f, "path") else str(f)} for f in self.files]
   mcp_client.configure(file_items_as_dicts, [str(new_root)])
   self.ai_status = f"switched to: {Path(path).stem}"
  finally:
   with self._project_switch_lock:
    pending = self._project_switch_pending_path
    self._project_switch_in_progress = False
    self._project_switch_pending_path = None
   if pending and pending != self.active_project_path and Path(pending).exists():
    self._switch_project(pending)

 def _switch_project(self, path: str) -> None:
  """
    [C: src/gui_2.py:App._render_projects_panel]
  
  Non-blocking: returns immediately, marks the controller as stale,
  and runs the actual save/load work in a background thread so the
  render loop keeps drawing and lightweight UI interactions (scrolling,
  selecting tabs) remain responsive.
  """
  if path == self.active_project_path and not self.is_project_stale():
   return
  if not Path(path).exists():
   self.ai_status = f"project file not found: {path}"
   return
  with self._project_switch_lock:
   if self._project_switch_in_progress:
    if self._project_switch_pending_path == path:
     return
    self._project_switch_pending_path = path
    self.ai_status = f"switch queued: {Path(path).stem} (waiting on {Path(self._project_switch_pending_path or '').stem})"
    return
   self._project_switch_in_progress = True
   self._project_switch_pending_path = path
  self.ai_status = f"switching to: {Path(path).stem} (stale ui - ops disabled)"
  self.submit_io(self._do_project_switch, path)

 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, tests/test_view_presets.py:test_load_presets_from_project_legacy_dict, tests/test_view_presets.py:test_load_presets_from_project_list]
  """
  raw_paths = self.project.get("files", {}).get("paths", [])
  self.files = []
  for p in raw_paths:
   if isinstance(p, models.FileItem):
    self.files.append(p)
   elif isinstance(p, dict):
    self.files.append(models.FileItem.from_dict(p))
   else:
    self.files.append(models.FileItem(path=str(p)))
  import copy
  self.context_files = []
  for f in self.files:
   if isinstance(f, models.FileItem):
    fi = copy.deepcopy(f)
   else:
    fi = models.FileItem(path=str(f))
   self.context_files.append(fi)
  if hasattr(self, "_app") and self._app is not None:
   self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
  self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
  disc_sec = self.project.get("discussion", {})
  self.disc_roles = list(disc_sec.get("roles", ["User", "AI", "Vendor API", "System"]))
  self.active_discussion = disc_sec.get("active", "main")
  disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
  with self._disc_entries_lock:
   self.disc_entries[:] = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
  proj = self.project
  self.ui_output_dir = proj.get("output", {}).get("output_dir", "./md_gen")
  self.ui_files_base_dir = proj.get("files", {}).get("base_dir", ".")
  self.ui_shots_base_dir = proj.get("screenshots", {}).get("base_dir", ".")
  proj_meta = self.project.get("project", {})
  self.ui_project_git_dir = proj_meta.get("git_dir", "")
  self.ui_project_system_prompt = proj_meta.get("system_prompt", "")
  self.ui_project_preset_name = proj_meta.get("active_preset")
  self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
  self.ui_auto_add_history = proj.get("discussion", {}).get("auto_add", False)
  self.ui_auto_scroll_comms = proj.get("project", {}).get("auto_scroll_comms", True)
  self.ui_auto_scroll_tool_calls = proj.get("project", {}).get("auto_scroll_tool_calls", True)
  self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
  agent_tools_cfg = proj.get("agent", {}).get("tools", {})
  self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
  # MMA Tracks
  self.tracks = project_manager.get_all_tracks(self.active_project_root)
  # Restore MMA state
  mma_sec = proj.get("mma", {})
  self.ui_epic_input = mma_sec.get("epic", "")
  tier_models = mma_sec.get("tier_models", {})
  for tier, data in tier_models.items():
   if tier in self.mma_tier_usage:
    self.mma_tier_usage[tier]["model"] = data.get("model", self.mma_tier_usage[tier]["model"])
    self.mma_tier_usage[tier]["provider"] = data.get("provider", self.mma_tier_usage[tier]["provider"])
    self.mma_tier_usage[tier]["tool_preset"] = data.get("tool_preset", self.mma_tier_usage[tier].get("tool_preset"))
  at_data = mma_sec.get("active_track")
  if at_data:
   try:
    tickets = []
    for t_data in at_data.get("tickets", []):
     tickets.append(models.Ticket(**t_data))
    self.active_track = models.Track(
     id=at_data.get("id"),
     description=at_data.get("description"),
     tickets=tickets
    )
    self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
   except Exception as e:
    print(f"Failed to deserialize active track: {e}")
    self.active_track = None
  else:
   self.active_track = None
   self.active_tickets = []
  # Load track-scoped history if track is active
  if self.active_track:
   track_history = project_manager.load_track_history(self.active_track.id, self.active_project_root)
   if track_history:
    with self._disc_entries_lock:
     self.disc_entries[:] = models.parse_history_entries(track_history, self.disc_roles)
  self.preset_manager.project_root = Path(self.active_project_root)
  self.presets = self.preset_manager.load_all()
  self.tool_preset_manager.project_root = Path(self.active_project_root)
  self.tool_presets = self.tool_preset_manager.load_all_presets()
  self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
  self.personas = self.persona_manager.load_all()
  if self.ui_active_persona and self.ui_active_persona not in self.personas:
   self.ui_active_persona = ""
  if self.ui_active_tool_preset and self.ui_active_tool_preset not in self.tool_presets:
   self.ui_active_tool_preset = None
  if self.ui_active_bias_profile and self.ui_active_bias_profile not in self.bias_profiles:
   self.ui_active_bias_profile = None
  if self.ui_project_preset_name and self.ui_project_preset_name not in self.presets:
   self.ui_project_preset_name = None
  ai_client.set_tool_preset(self.ui_active_tool_preset)
  ai_client.set_bias_profile(self.ui_active_bias_profile)
  raw_presets = proj.get("view_presets", [])
  if isinstance(raw_presets, dict):
   self.view_presets = [models.NamedViewPreset.from_dict({"name": name, **data}) for name, data in raw_presets.items()]
  else:
   self.view_presets = [models.NamedViewPreset.from_dict(p) for p in raw_presets if isinstance(p, dict)]
  if self.rag_config and self.rag_config.enabled:
   self._rebuild_rag_index()

 def is_project_stale(self) -> bool:
  """True when a project switch is queued or running; UI should tint
  to signal the controller state lags the user's last click."""
  if getattr(self, "_project_switch_in_progress", False):
   return True
  pending = getattr(self, "_project_switch_pending_path", None)
  if pending and pending != self.active_project_path:
   return True
  return False

 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)
    project_manager.save_project(cleaned, self.active_project_path)
   except Exception as e:
    self.ai_status = f"save error: {e}"

 #endregion: Project

 #region: Context

 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 _handle_clear_summary_cache(self, user_data: Any = None) -> None:
  self.summary_cache.clear()
  self.ai_status = 'summary cache cleared'

 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_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 save_context_preset(self, preset: models.ContextPreset) -> None:
  self.context_preset_manager.save_preset(self.project, preset)
  self._save_active_project()

 def load_context_preset(self, name: str) -> models.ContextPreset:
  presets = self.context_preset_manager.load_all(self.project)
  if name not in presets:
   raise KeyError(f"Context preset '{name}' not found.")
  preset = presets[name]

  # Update only temporary context state, not project files
  import copy
  self.context_files = []
  for f in preset.files:
   fi = models.FileItem(path=f.path, view_mode=f.view_mode)
   fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
   fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
   fi.ast_signatures = getattr(f, 'ast_signatures', False)
   fi.ast_definitions = getattr(f, 'ast_definitions', False)
   self.context_files.append(fi)

  self.screenshots = list(preset.screenshots)
  return preset

 def _update_cached_stats(self) -> None:
  from src import ai_client
  self._cached_cache_stats = ai_client.get_gemini_cache_stats()
  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()
 
 #endregion: Context

 #region: RAG

 def _set_rag_status(self, status: str) -> None:
  """Thread-safe update of rag_status via the GUI task queue."""
  with self._pending_gui_tasks_lock:
   self._pending_gui_tasks.append({
    "action": "set_value",
    "item": "rag_status",
    "value": status
    })

 def _rebuild_rag_index(self) -> None:
  """Background thread that re-indexes all files in the current project."""
  if not self.rag_config or not self.rag_config.enabled or not self.rag_engine:
   return

  def _run():
   try:
    self._set_rag_status("indexing...")
    import concurrent.futures

    # 1. Incremental indexing of current files in parallel
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
     futures = []
     def do_index(p):
      if self.rag_engine: self.rag_engine.index_file(p)
     for f in self.files:
      path = f.path if hasattr(f, "path") else str(f)
      futures.append(executor.submit(do_index, path))
     concurrent.futures.wait(futures)

    # 2. Cleanup stale entries (files no longer tracked)
    indexed_paths = self.rag_engine.get_all_indexed_paths()
    current_paths = {f.path if hasattr(f, "path") else str(f) for f in self.files}
    stale_paths = [p for p in indexed_paths if p not in current_paths]
    if stale_paths:
     self.rag_engine.delete_documents_by_path(stale_paths)

    self._set_rag_status("ready")
   except Exception as e:
    self._set_rag_status(f"error: {e}")

  self.submit_io(_run)

 #endregion: RAG

 #region: AI Settings

 def _fetch_models(self, provider: str) -> None:
  """
  [C: src/gui_2.py:App.run]
  """
  # In the desktop GUI, model listing imports the provider SDKs (the same
  # ~2s C-extension load warmup pays for). Defer it until the first frame is
  # painted so it doesn't contend for the GIL during window creation; the
  # deferred fetch is fired from start_warmup().
  if self._defer_warmup and not self._warmup_started:
   self._pending_fetch_provider = provider
   return
  self.ai_status = "fetching models..."

  def do_fetch() -> None:
   try:
    for p in ai_client.PROVIDERS:
     try:
      self.all_available_models[p] = ai_client.list_models(p)
     except Exception as e:
      self.all_available_models[p] = []

    models_list = self.all_available_models.get(provider, [])
    self.available_models = models_list
    if self.current_model not in models_list and models_list:
     self.current_model = models_list[0]
     ai_client.set_provider(self._current_provider, self.current_model)
    if self.ai_status == "fetching models...":
     self.ai_status = f"models loaded: {len(models_list)}"
   except Exception as e:
    if self.ai_status == "fetching models...":
     self.ai_status = f"model fetch error: {e}"
  self.submit_io(do_fetch)

 def _apply_preset(self, name: str, scope: str) -> None:
  """
    [C: src/gui_2.py:App._render_system_prompts_panel]
  """
  if name == "None":
   if scope == "global":
    self.ui_global_preset_name = ""
   else:
    self.ui_project_preset_name = ""
   return
  preset = self.presets.get(name)
  if not preset:
   return
  if scope == "global":
   self.ui_global_system_prompt = preset.system_prompt
   self.ui_global_preset_name = name
  else:
   self.ui_project_system_prompt = preset.system_prompt
   self.ui_project_preset_name = name

 def _cb_save_preset(self, name, content, scope):
  """
    [C: src/gui_2.py:App._render_preset_manager_content]
  """
  if not name or not name.strip():
   raise ValueError("Preset name cannot be empty or whitespace.")
  preset = models.Preset(
   name=name,
   system_prompt=content
  )
  self.preset_manager.save_preset(preset, scope)
  self.presets = self.preset_manager.load_all()

 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()

 def _cb_delete_bias_profile(self, name: str, scope: str = "project"):
  self.tool_preset_manager.delete_bias_profile(name, scope)
  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_save_view_preset(self, name: str, f_item: models.FileItem) -> None:
  """
    [C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_save_view_preset]
  """
  preset = models.NamedViewPreset(
   name=name,
   view_mode=f_item.view_mode,
   ast_mask=copy.deepcopy(f_item.ast_mask) if hasattr(f_item, "ast_mask") else {},
   custom_slices=copy.deepcopy(f_item.custom_slices) if hasattr(f_item, "custom_slices") else []
  )
  for i, vp in enumerate(self.view_presets):
   if vp.name == name:
    self.view_presets[i] = preset
    break
  else:
   self.view_presets.append(preset)
  self._flush_to_project()

 def _cb_apply_view_preset(self, name: str, f_item: models.FileItem) -> None:
  """
    [C: src/gui_2.py:App._render_context_files_table, tests/test_view_presets.py:test_apply_view_preset]
  """
  preset = next((vp for vp in self.view_presets if vp.name == name), None)
  if preset:
   f_item.view_mode = preset.view_mode
   f_item.ast_mask = copy.deepcopy(preset.ast_mask)
   f_item.custom_slices = copy.deepcopy(preset.custom_slices)

 def _cb_delete_view_preset(self, name: str) -> None:
  """
    [C: tests/test_view_presets.py:test_delete_view_preset]
  """
  self.view_presets = [vp for vp in self.view_presets if vp.name != name]
  self._flush_to_project()

 #endregion: AI Settings

 #region: Discusssion

 def _cb_disc_create(self) -> None:
  nm = self.ui_disc_new_name_input.strip()
  if nm:
   self._create_discussion(nm)
   self.ui_disc_new_name_input = ""

 def _get_discussion_names(self) -> list[str]:
  """
    [C: src/gui_2.py:App._render_discussion_selector, src/gui_2.py:App._render_theme_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_selector, src/gui_2.py:App._render_takes_panel, src/gui_2.py:App._render_theme_panel]
  """
  self._flush_disc_entries_to_project()
  disc_sec = self.project.get("discussion", {})
  discussions = disc_sec.get("discussions", {})
  if name not in discussions:
   self.ai_status = f"discussion not found: {name}"
   return
  self.active_discussion = name
  self._force_tab_selection = True
  self._track_discussion_active = False
  disc_sec["active"] = name
  disc_data = discussions[name]
  with self._disc_entries_lock:
   self.disc_entries[:] = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
  self.discussion_sent_markdown = disc_data.get("sent_markdown", "")
  self.discussion_sent_system_prompt = disc_data.get("sent_system_prompt", "")
  if "context_snapshot" in disc_data:
   snapshot_data = disc_data["context_snapshot"]
   self.context_files = [models.FileItem.from_dict(f) if isinstance(f, dict) else models.FileItem(path=str(f)) for f in snapshot_data]
   if self._app:
    self._app.ui_selected_context_files = {f.path for f in self.context_files if f.auto_aggregate}
  self.ai_status = f"discussion: {name}"

 def _flush_disc_entries_to_project(self) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_selector, src/gui_2.py:App._render_theme_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)
   return
  disc_sec = self.project.setdefault("discussion", {})
  discussions = disc_sec.setdefault("discussions", {})
  disc_data = discussions.setdefault(self.active_discussion, project_manager.default_discussion())
  disc_data["history"] = history_strings
  disc_data["last_updated"] = project_manager.now_ts()
  disc_data["context_snapshot"] = [f.to_dict() if hasattr(f, "to_dict") else {"path": str(f)} for f in self.context_files]
  disc_data["sent_markdown"] = getattr(self, "discussion_sent_markdown", "")
  disc_data["sent_system_prompt"] = getattr(self, "discussion_sent_system_prompt", "")

 def _handle_approve_ask(self) -> None:
  """
     Responds with approval for a pending /api/ask request.
    [C: src/gui_2.py:App._handle_approve_ask, src/gui_2.py:App._render_mma_modals]
  """
  if not self._ask_request_id: return
  request_id = self._ask_request_id

  def do_post() -> None:
   try:
    import requests
    requests.post(
     "http://127.0.0.1:8999/api/ask/respond",
     json={"request_id": request_id, "response": {"approved": True}},
     timeout=2
    )
   except Exception as e: print(f"Error responding to ask: {e}")
  self.submit_io(do_post)
  self._pending_ask_dialog = False
  self._ask_request_id = None
  self._ask_tool_data = None

 def _handle_reject_ask(self) -> None:
  """
     Responds with rejection for a pending /api/ask request.
    [C: src/gui_2.py:App._render_mma_modals]
  """
  if not self._ask_request_id: return
  request_id = self._ask_request_id

  def do_post() -> None:
   try:
    import requests
    requests.post(
     "http://127.0.0.1:8999/api/ask/respond",
     json={"request_id": request_id, "response": {"approved": False}},
     timeout=2
    )
   except Exception as e: print(f"Error responding to ask: {e}")
  self.submit_io(do_post)
  self._pending_ask_dialog = False
  self._ask_request_id = None
  self._ask_tool_data = None

 def _handle_reset_session(self) -> None:
  """
     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()
  self._tool_stats.clear()
  self._comms_log.clear()
  self.disc_entries.clear()
  self.discussion_sent_markdown = ""
  self.discussion_sent_system_prompt = ""
  self.files.clear()
  self.context_files.clear()
  self.tracks.clear()
  # Clear history in ALL discussions to be safe
  disc_sec = self.project.get("discussion", {})
  discussions = disc_sec.get("discussions", {})
  for d_name in discussions:
   discussions[d_name]["history"] = []
  # Reset project state so stale data from prior live_gui tests does not
  # leak into the next session. We do NOT clear self.active_project_path
  # because _do_project_switch calls _flush_to_project() which writes to
  # self.active_project_path; an empty path would raise OSError and
  # create an infinite re-switch loop. (See test_context_sim_live
  # regression on 2026-06-08.)
  self.project = project_manager.default_project(
   Path(self.active_project_path).stem if self.active_project_path else "unnamed"
  )
  self.project_paths = []
  # Clear project-switch state machine so a hung switch from a prior test
  # does not block the next session. (is_project_stale() must return False
  # for the next click to actually submit a new switch.)
  with self._project_switch_lock:
   self._project_switch_in_progress = False
   self._project_switch_pending_path = None
   self._project_switch_error = None
  self.ai_status = "session reset"
  self.ai_response = ""
  self.ui_ai_input = ""
  self.ui_manual_approve = False
  self.ui_auto_add_history = False
  self.active_track = None
  self.active_tier = None
  self.mma_status = 'idle'
  self.proposed_tracks = []
  self.active_tickets = []
  self.engines.clear()
  self.mma_streams.clear()
  # Reset mma_tier_usage to the same shape as __init__ (line 952-957). Prior
  # tests pollute it; downstream consumers like _flush_to_project require
  # every tier entry to have 'model' / 'provider' / 'tool_preset' keys. The
  # pre-populated defaults (input=0, output=0, provider='gemini', model=
  # tier default, tool_preset=None) restore the contract without retaining
  # any polluted model names or token counts from a prior session.
  self.mma_tier_usage = {
   "Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None},
   "Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None},
   "Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite",  "tool_preset": None},
   "Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite",  "tool_preset": None},
  }
  # Reset RAG engine state so the chroma collection from a prior test
  # doesn't leak into the next session. The next _sync_rag_engine will
  # rebuild the engine with the new active_project_root.
  # NOTE: rag_config is reset to a fresh default (not None) so the rag_*
  # setters can still mutate it. Setting it to None was a bug — setters
  # check `if self.rag_config:` and become no-ops, leaving the engine
  # unsynced forever (test_rag_phase4_final_verify regression on
  # 2026-06-10).
  self.rag_engine = None
  from src import models as _rag_models
  self.rag_config = _rag_models.RAGConfig()
  self.rag_status = 'idle'
  self._rag_sync_token = 0
  self._rag_sync_dirty = False
  self._worker_status.clear()
  self._current_provider = "gemini"
  self._current_model = "gemini-2.5-flash-lite"
  ai_client.set_provider(self._current_provider, self._current_model)
  with self._pending_history_adds_lock:
   self._pending_history_adds.clear()
  with self._api_event_queue_lock:
   self._api_event_queue.clear()
  with self._pending_gui_tasks_lock:
   self._pending_gui_tasks.clear()
  self.ui_use_default_base_prompt = True
  self.ui_global_system_prompt = ''
  self.ui_base_system_prompt = ''
  self.ui_project_system_prompt = ''
  self.ui_active_persona = ''
  self.ui_active_tool_preset = None
  self.ui_active_bias_profile = None
  self.temperature = 0.0
  self.top_p = 1.0
  self.max_tokens = 8192

 def _handle_compress_discussion(self) -> None:
  def worker():
   try:
    self.ai_status = "compressing discussion..."
    disc_text = project_manager.format_discussion(self.disc_entries)
    if not disc_text.strip():
     self.ai_status = "discussion is empty"
     return
    
    response_text = ai_client.run_discussion_compression(disc_text)
    
    if response_text and not response_text.startswith("ERROR:"):
     with self._disc_entries_lock:
      self.disc_entries.clear()
      self.disc_entries.append({"role": "System", "content": f"[COMPRESSED HISTORY]\n{response_text}", "collapsed": False, "ts": project_manager.now_ts()})
     self.ai_status = "compression complete"
    else:
     self.ai_status = f"compression failed: {response_text}"
   except Exception as e:
    self.ai_status = f"compression error: {e}"
  self.submit_io(worker)

 def _handle_generate_send(self) -> None:
  """
     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]
  """
  if self.is_project_stale():
   self.ai_status = "project switch in progress; AI ops disabled"
   return

  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, file_items, stable_md, disc_text = self._do_generate()
    self._last_stable_md = stable_md
    self.last_md = md
    self.last_md_path = path
    self.last_file_items = file_items
    self.ai_status = "sending..."
    user_msg = self.ui_ai_input

    base_dir = self.active_project_root
    # Prepare event payload
    event_payload = events.UserRequestEvent(
     prompt=user_msg,
     stable_md=stable_md,
     file_items=file_items,
     disc_text=disc_text,
     base_dir=base_dir
    )
    # Push to async queue
    self.event_queue.put("user_request", event_payload)
   except Exception as e:
    self.ai_status = f"generate error: {e}"
  self.submit_io(worker)

 def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
  """
  Returns (full_md, output_path, file_items, stable_md, discussion_text).
  [C: src/gui_2.py:App._show_menus, tests/test_context_composition_decoupled.py:test_do_generate_uses_context_files, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy]
  """
  self._flush_to_project()
  self._flush_to_config()
  self.save_config()
  track_id = self.active_track.id if self.active_track else None
  flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id)
  
  import copy
  flat["files"] = copy.copy(flat.get("files", {}))
  flat["files"]["paths"] = self.context_files

  import os
  file_dicts = []
  for f in self.context_files:
   p = f.path if hasattr(f, 'path') else str(f)
   if not os.path.isabs(p):
    p = os.path.join(self.ui_files_base_dir, p)
   file_dicts.append({"path": p})
  mcp_client.configure(file_dicts, [self.ui_files_base_dir])

  persona = self.personas.get(self.ui_active_persona)
  strategy = persona.aggregation_strategy if persona else "auto"

  full_md, path, file_items = aggregate.run(flat, aggregation_strategy=strategy)
  # Build stable markdown (no history) for Gemini caching
  screenshot_base_dir = Path(flat.get("screenshots", {}).get("base_dir", "."))
  screenshots = flat.get("screenshots", {}).get("paths", [])
  summary_only = flat.get("project", {}).get("summary_only", False)
  stable_md = aggregate.build_markdown_no_history(file_items, screenshot_base_dir, screenshots, summary_only=summary_only, aggregation_strategy=strategy)
  # Build discussion history text separately
  history = flat.get("discussion", {}).get("history", [])
  discussion_text = aggregate.build_discussion_text(history)

  csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
  ai_client.set_custom_system_prompt("\n\n".join(csp))
  ai_client.set_base_system_prompt(self.ui_base_system_prompt)
  ai_client.set_use_default_base_prompt(self.ui_use_default_base_prompt)
  ai_client.set_project_context_marker(self.ui_project_context_marker)
  self.last_resolved_system_prompt = ai_client.get_combined_system_prompt()
  self.last_aggregate_markdown = full_md

  return full_md, path, file_items, stable_md, discussion_text

 def _handle_md_only(self) -> None:
  """
  Logic for the 'MD Only' action.
  [C: src/gui_2.py:App._render_message_panel]
  """
  if self.is_project_stale():
   self.ai_status = "project switch in progress; MD generation disabled"
   return

  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
    self.last_md_path = path
    self.ai_status = f"md written: {path.name}"
    # Refresh token budget metrics with CURRENT md
    self._refresh_api_metrics({}, md_content=md)
   except Exception as e:
    self.ai_status = f"error: {e}"
  self.submit_io(worker)

 def _create_discussion(self, name: str) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_metadata, 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:
   self.ai_status = f"discussion '{name}' already exists"
   return
  new_disc = project_manager.default_discussion()
  # Inherit context from current session if available
  if self.context_files:
   new_disc["context_snapshot"] = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_files]
  discussions[name] = new_disc
  self._switch_discussion(name)

 def _branch_discussion(self, index: int) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_entry]
  """
  self._flush_disc_entries_to_project()
  # Generate a unique branch name
  base_name = self.active_discussion.split("_take_")[0]
  counter = 1
  new_name = f"{base_name}_take_{counter}"
  disc_sec = self.project.get("discussion", {})
  discussions = disc_sec.get("discussions", {})
  while new_name in discussions:
   counter += 1
   new_name = f"{base_name}_take_{counter}"

  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_metadata]
  """
  disc_sec = self.project.get("discussion", {})
  discussions = disc_sec.get("discussions", {})
  if old_name not in discussions:
   return
  if new_name in discussions:
   self.ai_status = f"discussion '{new_name}' already exists"
   return
  discussions[new_name] = discussions.pop(old_name)
  if self.active_discussion == old_name:
   self.active_discussion = new_name
   disc_sec["active"] = new_name

 def _delete_discussion(self, name: str) -> None:
  """
    [C: src/gui_2.py:App._render_discussion_metadata]
  """
  disc_sec = self.project.get("discussion", {})
  discussions = disc_sec.get("discussions", {})
  if len(discussions) <= 1:
   self.ai_status = "cannot delete the last discussion"
   return
  if name not in discussions:
   return
  del discussions[name]
  if self.active_discussion == name:
   remaining = sorted(discussions.keys())
   self._switch_discussion(remaining[0])

 #endregion: Discusssion

#region: Operations

 def _handle_request_event(self, event: events.UserRequestEvent) -> None:
  """
  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...'
  
  user_msg = event.prompt
  
  # 1. RAG Retrieval (Enrich prompt before logging to history)
  if self.rag_engine and self.rag_config and self.rag_config.enabled:
   try:
    chunks = self.rag_engine.search(user_msg)
    if chunks:
     context_block = "## Retrieved Context\n\n"
     for i, chunk in enumerate(chunks):
      path = chunk.get("metadata", {}).get("path", "unknown")
      context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n"
     user_msg = context_block + user_msg
   except Exception as e:
    sys.stderr.write(f"RAG search error: {e}\n")
    sys.stderr.flush()

  # 2. Symbol Resolution (Enrich prompt before logging to history)
  try:
   symbols = parse_symbols(user_msg)
   file_paths = [f['path'] for f in event.file_items]
   for symbol in symbols:
    res = get_symbol_definition(symbol, file_paths)
    if res:
     file_path, definition, line = res
     user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```'
  except Exception as e:
   sys.stderr.write(f"Symbol resolution error: {e}\n")
   sys.stderr.flush()

  # 3. Log the final enriched prompt to history
  self.event_queue.put("comms", {
   "kind": "request",
   "payload": {
    "message": user_msg,
    "collapsed": False
   }
  })

  ai_client.set_current_tier(None) # Ensure main discussion is untagged
  # Clear response area for new turn
  self.ai_response = ""
  csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
  custom_prompt = "\n\n".join(csp)
  ai_client.set_custom_system_prompt(custom_prompt)
  ai_client.set_base_system_prompt(self.ui_base_system_prompt)
  ai_client.set_use_default_base_prompt(self.ui_use_default_base_prompt)
  ai_client.set_project_context_marker(self.ui_project_context_marker)
  self.last_resolved_system_prompt = ai_client.get_combined_system_prompt()
  self.discussion_sent_markdown = event.stable_md
  self.discussion_sent_system_prompt = self.last_resolved_system_prompt
  ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit, self.top_p)
  ai_client.set_agent_tools(self.ui_agent_tools)  # Force update adapter path right before send to bypass potential duplication issues
  self._update_gcli_adapter(self.ui_gemini_cli_path)
  # FR2 / Bug #1: per conductor/code_styleguides/error_handling.md section 3.1 (AND over OR),
  # we check result.ok instead of catching a ProviderError exception.
  result = ai_client.send_result(
   event.stable_md,
   user_msg,
   event.base_dir,
   event.file_items,
   event.disc_text,
   stream=True,
   stream_callback=lambda text: self._on_ai_stream(text),
   pre_tool_callback=self._confirm_and_run,
   qa_callback=ai_client.run_tier4_analysis,
   patch_callback=ai_client.run_tier4_patch_callback,
   rag_engine=None, # Already handled above
  )
  if result.ok:
   self.event_queue.put("response", {"text": result.data, "status": "done", "role": "AI"})
   self._ai_status = "done"
  else:
   err = result.errors[0]
   self.event_queue.put("response", {"text": err.ui_message(), "status": "error", "role": "Vendor API"})
   self._ai_status = f"error: {err.ui_message()}"

 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()
  with self._pending_tool_calls_lock:
   self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})

 def _offload_entry_payload(self, entry: Dict[str, Any]) -> Dict[str, Any]:
  optimized = copy.deepcopy(entry)
  kind = optimized.get("kind")
  payload = optimized.get("payload", {})
  if kind == "tool_result" and "output" in payload:
   output = payload["output"]
   ref_path = session_logger.log_tool_output(output)
   if ref_path:
    filename = Path(ref_path).name
    payload["output"] = f"[REF:{filename}]"
  if kind == "tool_call" and "script" in payload:
   script = payload["script"]
   ref_path = session_logger.log_tool_call(script, "LOG_ONLY", None)
   if ref_path:
    filename = Path(ref_path).name
    payload["script"] = f"[REF:{filename}]"
  return optimized

 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)
  if self.test_hooks_enabled:
   with self._api_event_queue_lock:
    self._api_event_queue.append({"type": event_name, "payload": payload})

 def _on_ai_stream(self, text: str) -> None:
  """Handles streaming text from the AI."""
  self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
  from src.vendor_capabilities import get_capabilities
  try:
   caps = get_capabilities(self.current_provider, self.current_model)
  except KeyError:
   caps = None
  if caps is None or caps.streaming:
   if self._ai_status not in ("sending...", "streaming..."):
    self._ai_status = "streaming..."
 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()
  kind = entry.get("kind")
  payload = entry.get("payload", {})

  if kind == "response" and "usage" in payload:
   u = payload["usage"]
   inp = u.get("input_tokens") or u.get("prompt_tokens") or 0
   out = u.get("output_tokens") or u.get("completion_tokens") or 0
   cache_read = u.get("cache_read_input_tokens") or 0
   cache_create = u.get("cache_creation_input_tokens") or 0
   total = u.get("total_tokens") or 0

   # Store normalized usage back in payload for history rendering
   u["input_tokens"] = inp
   u["output_tokens"] = out
   u["cache_read_input_tokens"] = cache_read

   self.session_usage["input_tokens"] += inp
   self.session_usage["output_tokens"] += out
   self.session_usage["cache_read_input_tokens"] += cache_read
   self.session_usage["cache_creation_input_tokens"] += cache_create
   self.session_usage["total_tokens"] += total
   input_t = u.get("input_tokens") or 0
   output_t = u.get("output_tokens") or 0
   model = payload.get("model", "unknown")
   self._token_history.append({
    "time": time.time(),
    "input": input_t,
    "output": output_t,
    "model": model
   })

  if kind == "request":
   if self.ui_auto_add_history:
    with self._pending_history_adds_lock:
     self._pending_history_adds.append({
      "role": "User",
      "content": payload.get("message", ""),
      "collapsed": payload.get("collapsed", False),
      "ts": entry.get("ts", project_manager.now_ts())
     })

  if kind == "response":
   if self.ui_auto_add_history:
    role = payload.get("role", "AI")
    text_content = payload.get("text", "")
    if text_content.strip():
     segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
     entry_obj = {
      "role": role,
      "content": parsed_response.strip() if parsed_response else "",
      "collapsed": True,
      "ts": entry.get("ts", project_manager.now_ts())
     }
     if "usage" in payload:
      entry_obj["usage"] = payload["usage"]
     if segments:
      entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]

     if entry_obj["content"] or segments:
      with self._pending_history_adds_lock:
       self._pending_history_adds.append(entry_obj)

  if kind in ("tool_result", "tool_call"):
   if self.ui_auto_add_history:
    role = "Tool" if kind == "tool_result" else "Vendor API"
    content = ""
    if kind == "tool_result":
     content = payload.get("output", "")
    else:
     content = payload.get("script") or payload.get("args") or payload.get("message", "")
     if isinstance(content, dict):
      content = json.dumps(content, indent=1)
    with self._pending_history_adds_lock:
     self._pending_history_adds.append({
      "role": role,
      "content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
      "collapsed": True,
      "ts": entry.get("ts", project_manager.now_ts())
      })
  if kind == "history_add":
   payload = entry.get("payload", {})
   with self._pending_history_adds_lock:
    self._pending_history_adds.append({
     "role": payload.get("role", "AI"),
     "content": payload.get("content", ""),
     "collapsed": payload.get("collapsed", False),
     "ts": entry.get("ts", project_manager.now_ts())
     })
   return
  with self._pending_comms_lock:
   self._pending_comms.append(entry)

 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()
  if tool_name:
   if tool_name not in self._tool_stats:
    self._tool_stats[tool_name] = {"count": 0, "total_time_ms": 0.0, "failures": 0}
   self._tool_stats[tool_name]["count"] += 1
   self._tool_stats[tool_name]["total_time_ms"] += elapsed_ms
   if is_failure:
    self._tool_stats[tool_name]["failures"] += 1
  self.ui_last_script_text = script
  self.ui_last_script_output = result
  self._trigger_script_blink = True
  self.show_script_output = True
  if self.ui_auto_scroll_tool_calls:
   self._scroll_tool_calls_to_bottom = True

 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)
   self.ai_status = "powershell done, awaiting AI..."
   return output
  dialog = ConfirmDialog(script, base_dir)
  is_headless = "--headless" in sys.argv
  if is_headless:
   with self._pending_dialog_lock:
    self._pending_actions[dialog._uid] = dialog
  else:
   with self._pending_dialog_lock:
    self._pending_dialog = dialog
  if self.test_hooks_enabled and hasattr(self, '_api_event_queue'):
   with self._api_event_queue_lock:
    self._api_event_queue.append({
     "type": "script_confirmation_required",
     "action_id": dialog._uid,
     "script": str(script),
     "base_dir": str(base_dir),
     "ts": time.time()
     })
  approved, final_script = dialog.wait()
  if is_headless:
   with self._pending_dialog_lock:
    if dialog._uid in self._pending_actions:
     del self._pending_actions[dialog._uid]
  if not approved:
   self._append_tool_log(final_script, "REJECTED by user")
   return None
  self.ai_status = "running powershell..."
  output = shell_runner.run_powershell(final_script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
  self._append_tool_log(final_script, output)
  self.ai_status = "powershell done, awaiting AI..."
  return output

 def resolve_pending_action(self, action_id: str, approved: bool) -> bool:
  with self._pending_dialog_lock:
   if action_id in self._pending_actions:
    dialog = self._pending_actions[action_id]
    with dialog._condition:
     dialog._approved = approved
     dialog._done = True
     dialog._condition.notify_all()
    return True
   elif self._pending_dialog and self._pending_dialog._uid == action_id:
    dialog = self._pending_dialog
    with dialog._condition:
     dialog._approved = approved
     dialog._done = True
     dialog._condition.notify_all()
    return True
  return False

 def _extract_tool_name(self, script: str) -> str:
  if not script:
   return "unknown"
  script_lower = script.lower()
  if "powershell" in script_lower or "run_powershell" in script_lower:
   return "run_powershell"
  if "read_file" in script_lower:
   return "read_file"
  if "write_file" in script_lower or "write" in script_lower:
   return "write_file"
  if "list_directory" in script_lower or "ls" in script_lower:
   return "list_directory"
  if "search_files" in script_lower or "glob" in script_lower:
   return "search_files"
  if "web_search" in script_lower:
   return "web_search"
  if "fetch_url" in script_lower:
   return "fetch_url"
  if "py_get" in script_lower:
   return "py_get_skeleton"
  return "other"

 def _handle_approve_script(self, user_data=None) -> None:
  """Approves the currently pending PowerShell script."""
  with self._pending_dialog_lock:
   dlg = self._pending_dialog
   if dlg:
    with dlg._condition:
     dlg._approved = True
     dlg._done = True
     dlg._condition.notify_all()
    self._pending_dialog = None

 def _handle_reject_script(self, user_data=None) -> None:
  """Rejects the currently pending PowerShell script."""
  with self._pending_dialog_lock:
   dlg = self._pending_dialog
   if dlg:
    with dlg._condition:
     dlg._approved = False
     dlg._done = True
     dlg._condition.notify_all()
    self._pending_dialog = None

#endregion: Operations

 #region MMA (Controller)

 def _process_event_queue(self) -> None:
  """Listens for and processes events from the SyncEventQueue."""
  while True:
   event_name, payload = self.event_queue.get()
   if event_name == "shutdown":
    break
   if event_name == "user_request":
    self.submit_io(self._handle_request_event, payload)
   elif event_name == "gui_task":
    with self._pending_gui_tasks_lock:
    # Directly append the task from the hook server.
    # It already contains 'action' and any necessary fields.
     self._pending_gui_tasks.append(payload)
   elif event_name == "mma_state_update":
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "mma_state_update",
      "payload": payload
      })
   elif event_name == "mma_stream":
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "mma_stream_append",
      "payload": payload
      })
   elif event_name in ("mma_spawn_approval", "mma_step_approval"):
    with self._pending_gui_tasks_lock:
    # These payloads already contain the 'action' field
     self._pending_gui_tasks.append(payload)
   elif event_name == "response":
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "handle_ai_response",
      "payload": payload
      })
    if self.test_hooks_enabled:
     with self._api_event_queue_lock:
      self._api_event_queue.append({"type": "response", "payload": payload})
   elif event_name == "ticket_started":
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "ticket_started",
      "payload": payload
      })
   elif event_name == "ticket_completed":
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "ticket_completed",
      "payload": payload
      })
   elif event_name == "refresh_external_mcps":
    import asyncio
    asyncio.run(self.refresh_external_mcps())

 def _cb_plan_epic(self) -> None:
  """
    [C: src/gui_2.py:App._render_mma_epic_planner, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread]
  """
  def _bg_task() -> None:
   try:
    self.ai_status = "Planning Epic (Tier 1)..."
    history = orchestrator_pm.get_track_history_summary()
    proj = project_manager.load_project(self.active_project_path)
    flat = project_manager.flat_config(self.project)
    flat.setdefault("files", {})["paths"] = self.context_files
    file_items = aggregate.build_file_items(Path(self.active_project_root), flat.get("files", {}).get("paths", []))

    _t1_baseline = len(ai_client.get_comms_log())
    tracks = orchestrator_pm.generate_tracks(self.ui_epic_input, flat, file_items, history_summary=history)
    _t1_new = ai_client.get_comms_log()[_t1_baseline:]
    _t1_resp = [e for e in _t1_new if e.get("direction") == "IN" and e.get("kind") == "response"]
    _t1_in = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _t1_resp)
    _t1_out = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _t1_resp)

    def _push_t1_usage(i: int, o: int) -> None:
     self.mma_tier_usage["Tier 1"]["input"] += i
     self.mma_tier_usage["Tier 1"]["output"] += o
    with self._pending_gui_tasks_lock:
     self._pending_gui_tasks.append({
      "action": "custom_callback",
      "callback": _push_t1_usage,
      "args": [_t1_in, _t1_out]
      })
     self._pending_gui_tasks.append({
      "action": "handle_ai_response",
      "payload": {
       "text": json.dumps(tracks, indent=2),
       "stream_id": "Tier 1",
       "status": "Epic tracks generated."
      }
      })
     self._pending_gui_tasks.append({
      "action": "show_track_proposal",
      "payload": tracks
      })
   except Exception as e:
    self.ai_status = f"Epic plan error: {e}"
    print(f"ERROR in _cb_plan_epic background task: {e}")
  self.submit_io(_bg_task)

 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:
   # Generate skeletons once
   self.ai_status = "Phase 2: Generating skeletons for all tracks..."
   parser = ASTParser(language="python")
   generated_skeletons = ""
   try:
   # Use a local copy of files to avoid concurrent modification issues
    files_to_scan = list(self.files)
    for i, f_item in enumerate(files_to_scan):
     try:
      self.ai_status = f"Phase 2: Scanning files ({i+1}/{len(files_to_scan)})..."
      f_path = f_item.path if hasattr(f_item, 'path') else str(f_item)
      abs_path = Path(self.active_project_root) / f_path
      if abs_path.exists() and abs_path.suffix == ".py":
       with open(abs_path, "r", encoding="utf-8") as f:
        code = f.read()
        generated_skeletons += f"\nFile: {f_path}\n{parser.get_skeleton(code)}\n"
     except Exception as e:
      pass
   except Exception as e:
    self.ai_status = f"Error generating skeletons: {e}"
    return # Exit if skeleton generation fails
   # Now loop through tracks and call _start_track_logic with generated skeletons
   total_tracks = len(self.proposed_tracks)
   print(f"[DEBUG] _cb_accept_tracks: Starting {total_tracks} tracks...")
   for i, track_data in enumerate(self.proposed_tracks):
    title = track_data.get("title") or track_data.get("goal", "Untitled Track")
    self.ai_status = f"Processing track {i+1} of {total_tracks}: '{title}'..."
    self._start_track_logic(track_data, skeletons_str=generated_skeletons) # Pass skeletons
   print(f"[DEBUG] _cb_accept_tracks: All {total_tracks} tracks processed.")
   with self._pending_gui_tasks_lock:
    self._pending_gui_tasks.append({'action': 'refresh_from_project'}) # Ensure UI refresh after tracks are started
   self.ai_status = f"All {total_tracks} tracks accepted and execution started."
  self.submit_io(_bg_task)

 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
   # Ensure it's loaded as active
   if not self.active_track or self.active_track.id != track_id:
    self._cb_load_track(track_id)
   if self.active_track and self.active_track.id == track_id:
   # Use the active track object directly to start execution
    self.mma_status = "running"
    _mma_cfg = self.config.get("mma", {})
    _max_workers = int(_mma_cfg.get("max_workers", 4))
    engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode, max_workers=_max_workers)
    self.engines[self.active_track.id] = engine
    flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id)
    full_md, _, _ = aggregate.run(flat)
    self.submit_io(engine.run, md_content=full_md)
    self.ai_status = f"Track '{self.active_track.description}' started."
   elif self.active_track and self.active_track.id != track_id:
   # load_track failed but active_track is still wrong - reload explicitly
    self._cb_load_track(track_id)
    if self.active_track and self.active_track.id == track_id:
     self.mma_status = "running"
     _mma_cfg = self.config.get("mma", {})
     _max_workers = int(_mma_cfg.get("max_workers", 4))
     engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode, max_workers=_max_workers)
     self.engines[self.active_track.id] = engine
     flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id)
     full_md, _, _ = aggregate.run(flat)
     self.submit_io(engine.run, md_content=full_md)
     self.ai_status = f"Track '{self.active_track.description}' started."
   return
  idx = 0
  if isinstance(user_data, int):
   idx = user_data
  elif isinstance(user_data, dict):
   idx = user_data.get("index", 0)
  if 0 <= idx < len(self.proposed_tracks):
   track_data = self.proposed_tracks[idx]
   title = track_data.get("title") or track_data.get("goal", "Untitled Track")
   self.submit_io(lambda: self._start_track_logic(track_data))
   self.ai_status = f"Track '{title}' started."

 def _start_track_logic(self, track_data: dict[str, Any], skeletons_str: str | None = None) -> None:
  try:
   goal = track_data.get("goal", "")
   title = track_data.get("title") or track_data.get("goal", "Untitled Track")
   self.ai_status = f"Phase 2: Generating tickets for {title}..."
   skeletons = skeletons_str or "" # Use provided skeletons or empty
   self.ai_status = "Phase 2: Calling Tech Lead..."
   _t2_baseline = len(ai_client.get_comms_log())
   raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
   _t2_new = ai_client.get_comms_log()[_t2_baseline:]
   _t2_resp = [e for e in _t2_new if e.get("direction") == "IN" and e.get("kind") == "response"]
   _t2_in = sum(e.get("payload", {}).get("usage", {}).get("input_tokens", 0) for e in _t2_resp)
   _t2_out = sum(e.get("payload", {}).get("usage", {}).get("output_tokens", 0) for e in _t2_resp)

   def _push_t2_usage(i: int, o: int) -> None:
    self.mma_tier_usage["Tier 2"]["input"] += i
    self.mma_tier_usage["Tier 2"]["output"] += o
   with self._pending_gui_tasks_lock:
    self._pending_gui_tasks.append({
     "action": "custom_callback",
     "callback": _push_t2_usage,
     "args": [_t2_in, _t2_out]
     })
   if not raw_tickets:
    self.ai_status = f"Error: No tickets generated for track: {title}"
    print(f"Warning: No tickets generated for track: {title}")
    return
   self.ai_status = "Phase 2: Sorting tickets..."
   try:
    sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets)
   except ValueError as e:
    print(f"Dependency error in track '{title}': {e}")
    sorted_tickets_data = raw_tickets
    # 3. Create Track and Ticket objects
   tickets = []
   for t_data in sorted_tickets_data:
    ticket = models.Ticket(
     id=t_data["id"],
     description=t_data.get("description") or t_data.get("goal", "No description"),
     status=t_data.get("status", "todo"),
     assigned_to=t_data.get("assigned_to", "unassigned"),
     depends_on=t_data.get("depends_on", []),
     step_mode=t_data.get("step_mode", False)
    )
    tickets.append(ticket)
   track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}').hex[:12]}"
   track = models.Track(id=track_id, description=title, tickets=tickets)
   # Initialize track state in the filesystem
   meta = models.Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
   state = models.TrackState(metadata=meta, discussion=[], tasks=tickets)
   project_manager.save_track_state(track_id, state, self.active_project_root)
   # Add to memory and notify UI
   self.tracks.append({"id": track_id, "title": title, "status": "todo"})
   with self._pending_gui_tasks_lock:
    self._pending_gui_tasks.append({'action': 'refresh_from_project'})
   # 4. Initialize ConductorEngine and run loop
   sys.stderr.write(f"[DEBUG] _start_track_logic: Initializing engine for {track_id}...\n")
   sys.stderr.flush()
   _mma_cfg = self.config.get("mma", {})
   _max_workers = int(_mma_cfg.get("max_workers", 4))
   engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode, max_workers=_max_workers)
   self.engines[track.id] = engine
   # Use current full markdown context for the track execution
   track_id_param = track.id
   flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param)
   flat.setdefault("files", {})["paths"] = self.context_files
   sys.stderr.write(f"[DEBUG] _start_track_logic: Aggregating context for {track_id}...\n")
   sys.stderr.flush()
   full_md, _, _ = aggregate.run(flat)
   sys.stderr.write(f"[DEBUG] _start_track_logic: Starting engine thread for {track_id}...\n")
   sys.stderr.flush()
   # Start the engine in a separate thread
   self.submit_io(engine.run, md_content=full_md)
   sys.stderr.write(f"[DEBUG] _start_track_logic: Engine thread spawned for {track_id}.\n")
   sys.stderr.flush()
  except Exception as e:
   self.ai_status = f"Track start error: {e}"
   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'
    break
  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'
    break
  self.event_queue.put("mma_skip", {"ticket_id": ticket_id})

 def _spawn_worker(self, ticket_id: str, data: dict = None) -> None:
  """Manually initiates a sub-agent execution for a ticket."""
  engine = self.engines.get(self.active_track.id if self.active_track else None)
  if engine:
   for t in self.active_track.tickets:
    if t.id == ticket_id:
     t.status = "todo"
     t.step_mode = False
     break
   engine.engine.auto_queue = True
  self.event_queue.put("mma_retry", {"ticket_id": ticket_id})

 def kill_worker(self, worker_id: str) -> None:
  """
  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)

 def pause_mma(self) -> None:
  """Pauses the global MMA loop."""
  self.mma_step_mode = True
  engine = self.engines.get(self.active_track.id if self.active_track else None)
  if engine:
   engine.pause()

 def resume_mma(self) -> None:
  """Resumes the global MMA loop."""
  self.mma_step_mode = False
  engine = self.engines.get(self.active_track.id if self.active_track else None)
  if engine:
   engine.resume()

 def approve_ticket(self, ticket_id: str) -> None:
  """Manually approves a ticket for execution."""
  engine = self.engines.get(self.active_track.id if self.active_track else None)
  if engine and engine.engine:
   engine.engine.approve_task(ticket_id)
  else:
   # Fallback if engine not running
   for t in self.active_tickets:
    if t.get('id') == ticket_id:
     t['status'] = 'in_progress'
     break
  self._push_mma_state_update()

 def mutate_dag(self, data: dict) -> None:
  """Modifies task dependencies."""
  ticket_id = data.get("ticket_id")
  depends_on = data.get("depends_on")
  if ticket_id and depends_on is not None:
   for t in self.active_tickets:
    if t.get("id") == ticket_id:
     t["depends_on"] = depends_on
     break
   if self.active_track:
    for t in self.active_track.tickets:
     if t.id == ticket_id:
      t.depends_on = depends_on
      break
    engine = self.engines.get(self.active_track.id if self.active_track else None)
    if engine:
     from src.dag_engine import TrackDAG, ExecutionEngine
     engine.dag = TrackDAG(self.active_track.tickets)
     engine.engine = ExecutionEngine(engine.dag, auto_queue=engine.engine.auto_queue)
   self._push_mma_state_update()

 def _cb_run_conductor_setup(self) -> None:
  """
    [C: src/gui_2.py:App._render_mma_conductor_setup, 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."
   return
  files = list(base.glob("**/*"))
  files = [f for f in files if f.is_file()]
  summary = [f"Conductor Directory: {base.absolute()}"]
  summary.append(f"Total Files: {len(files)}")
  total_lines = 0
  for f in files:
   try:
    with open(f, "r", encoding="utf-8") as fd:
     lines = len(fd.readlines())
     total_lines += lines
     summary.append(f"- {f.relative_to(base)}: {lines} lines")
   except Exception:
    summary.append(f"- {f.relative_to(base)}: Error reading")
  summary.append(f"Total Line Count: {total_lines}")
  tracks_dir = base / "tracks"
  if tracks_dir.exists():
   tracks = [d for d in tracks_dir.iterdir() if d.is_dir()]
   summary.append(f"Total Tracks Found: {len(tracks)}")
  else:
   summary.append("Tracks Directory: Not found")
  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_track_browser, 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}"
  track_dir = paths.get_track_state_dir(track_id, project_path=self.active_project_root)
  track_dir.mkdir(parents=True, exist_ok=True)
  spec_file = track_dir / "spec.md"
  with open(spec_file, "w", encoding="utf-8") as f:
   f.write(f"# Specification: {name}\n\nType: {track_type}\n\nDescription: {desc}\n")
  plan_file = track_dir / "plan.md"
  with open(plan_file, "w", encoding="utf-8") as f:
   f.write(f"# Implementation Plan: {name}\n\n- [ ] Task 1: Initialize\n")
  meta_file = track_dir / "metadata.json"
  with open(meta_file, "w", encoding="utf-8") as f:
   json.dump({
    "id": track_id,
    "title": name,
    "description": desc,
    "type": track_type,
    "status": "new",
    "progress": 0.0
    }, f, indent=1)
   # Refresh tracks from disk
  self.tracks = project_manager.get_all_tracks(self.active_project_root)

 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._handle_approve_mma_step, src/gui_2.py:App._handle_approve_spawn, src/gui_2.py:App._render_mma_modals]
  """
  if self._pending_mma_approvals:
   task = self._pending_mma_approvals.pop(0)
   dlg = task.get("dialog_container", [None])[0]
   if dlg:
    with dlg._condition:
     dlg._approved = approved
     if payload is not None:
      dlg._payload = payload
     dlg._done = True
     dlg._condition.notify_all()
  elif self._pending_mma_spawns:
   task = self._pending_mma_spawns.pop(0)
   spawn_dlg = task.get("dialog_container", [None])[0]
   if spawn_dlg:
    with spawn_dlg._condition:
     spawn_dlg._approved = approved
     spawn_dlg._abort = abort
     if prompt is not None:
      spawn_dlg._prompt = prompt
     if context_md is not None:
      spawn_dlg._context_md = context_md
     spawn_dlg._done = True
     spawn_dlg._condition.notify_all()

 def _cb_load_track(self, track_id: str) -> None:
  """
    [C: src/gui_2.py:App._render_mma_track_browser]
  """
  state = project_manager.load_track_state(track_id, self.active_project_root)
  if state:
   try:
   # Convert list[Ticket] or list[dict] to list[Ticket] for Track object
    tickets = []
    for t in state.tasks:
     if isinstance(t, dict):
      tickets.append(models.Ticket(**t))
     else:
      tickets.append(t)
    self.active_track = models.Track(
     id=state.metadata.id,
     description=state.metadata.name,
     tickets=tickets
    )
    # Keep dicts for UI table
    self._load_active_tickets()
    # Load track-scoped history
    history = project_manager.load_track_history(track_id, self.active_project_root)
    with self._disc_entries_lock:
     if history:
      self.disc_entries[:] = models.parse_history_entries(history, self.disc_roles)
     else:
      self.disc_entries.clear()
    self._recalculate_session_usage()
    self.ai_status = f"Loaded track: {state.metadata.name}"
   except Exception as e:
    self.ai_status = f"Load track error: {e}"
    print(f"Error loading track {track_id}: {e}")

 def _push_mma_state_update(self) -> None:
  """
  Push the current MMA state to the project file. Called after any
  mutation (ticket status change, bulk execute, reorder, etc.) so
  the in-memory state (self.active_track.tickets) and the on-disk
  state match self.active_tickets.
  [C: tests/test_gui_phase4.py:test_push_mma_state_update, tests/test_ticket_queue.py:TestBulkOperations, tests/test_ticket_queue.py:TestReorder]
  """
  try:
   from src import project_manager
   track = self.active_track
   if track is None: return
   new_tickets = [
    models.Ticket(
     id=t.get("id", ""),
     description=t.get("description", ""),
     status=t.get("status", "todo"),
     assigned_to=t.get("assigned_to", ""),
     depends_on=t.get("depends_on", []),
    )
    for t in self.active_tickets
   ]
   track.tickets = new_tickets
   state = models.TrackState(metadata=track, tasks=list(new_tickets))
   project_manager.save_track_state(track.id, state, self.active_project_root)
  except Exception as e:
   import sys
   print(f"Error pushing MMA state: {e}", file=sys.stderr)

 def _load_active_tickets(self) -> None:
  """
  Load active tickets from the configured source. If execution_mode
  is "beads", read from the Beads repo at ui_files_base_dir.
  Otherwise, read from project state. The current code paths
  (mutate_dag, _cb_ticket_skip, etc.) populate self.active_tickets
  directly, so this method is the bootstrap path.
  [C: src/app_controller.py:_load_active_tickets call sites, tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads]
  """
  self.active_tickets = []
  if getattr(self, "ui_project_execution_mode", None) == "beads":
   base = getattr(self, "ui_files_base_dir", None) or getattr(self, "active_project_root", None)
   if base:
    try:
     from src import beads_client
     bclient = beads_client.BeadsClient(Path(base))
     if bclient.is_initialized():
      for bead in bclient.list_beads():
       self.active_tickets.append({
        "id":          bead.id,
        "title":       bead.title,
        "description": bead.description,
        "status":      bead.status,
        "depends_on":  [],
       })
    except Exception as e:
     print(f"Error loading beads: {e}")

 #region: --- Config I/O (single source of truth) ---
 def load_config(self) -> Dict[str, Any]:
  """
  Re-read the global config.toml from disk and update self.config.
  Returns the dict (also stored in self.config). Single source of
  truth for the in-memory config is self.config. Direct callers
  from outside the controller (e.g. models.load_config) are an
  architectural smell and will be flagged by
  scripts/audit_no_models_config_io.py.
  [C: src/app_controller.py:AppController.__init__]
  """
  self.config = models._load_config_from_disk()
  return self.config

 def save_config(self) -> None:
  """
  Flush self.config to disk. Single source of truth = self.config.
  This method owns the write path. Direct callers from outside the
  controller (e.g. models.save_config) are an architectural smell
  and will be flagged by
  scripts/audit_no_models_config_io.py.
  [C: src/app_controller.py:AppController._cb_project_save, src/app_controller.py:AppController._do_generate]
  """
  models._save_config_to_disk(self.config)
 #endregion: --- Config I/O (single source of truth) ---

 #endregion: MMA (Controller)

#region: MMA

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_ai_server.py:test_server_handles_list_models, tests/test_ai_server.py:test_server_handles_unknown_method, tests/test_ai_server.py:test_server_loads_google_genai_quickly, tests/test_ai_server.py:test_server_outputs_ready_marker, tests/test_ai_server.py:test_server_starts_and_exits_cleanly, 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:
    if time.time() - start_time > 120:
     return False, self._payload
    self._condition.wait(timeout=0.1)
  return self._approved, self._payload

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()
  self._done = False
  self._approved = False
  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_ai_server.py:test_server_handles_list_models, tests/test_ai_server.py:test_server_handles_unknown_method, tests/test_ai_server.py:test_server_loads_google_genai_quickly, tests/test_ai_server.py:test_server_outputs_ready_marker, tests/test_ai_server.py:test_server_starts_and_exits_cleanly, 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:
    if time.time() - start_time > 120:
     return {'approved': False, 'abort': True, 'prompt': self._prompt, 'context_md': self._context_md}
    self._condition.wait(timeout=0.1)
  return {
   'approved': self._approved,
   'abort': self._abort,
   'prompt': self._prompt,
   'context_md': self._context_md
  }

#endregion: MMA
