refactor(tests): Update test suite and API hooks for AppController architecture
This commit is contained in:
256
src/api_hooks.py
256
src/api_hooks.py
@@ -7,6 +7,26 @@ from typing import Any
|
||||
import logging
|
||||
import session_logger
|
||||
|
||||
def _get_app_attr(app: Any, name: str, default: Any = None) -> Any:
|
||||
if hasattr(app, name):
|
||||
return getattr(app, name)
|
||||
if hasattr(app, 'controller') and hasattr(app.controller, name):
|
||||
return getattr(app.controller, name)
|
||||
return default
|
||||
|
||||
def _has_app_attr(app: Any, name: str) -> bool:
|
||||
if hasattr(app, name): return True
|
||||
if hasattr(app, 'controller') and hasattr(app.controller, name): return True
|
||||
return False
|
||||
|
||||
def _set_app_attr(app: Any, name: str, value: Any) -> None:
|
||||
if hasattr(app, name):
|
||||
setattr(app, name, value)
|
||||
elif hasattr(app, 'controller'):
|
||||
setattr(app.controller, name, value)
|
||||
else:
|
||||
setattr(app, name, value)
|
||||
|
||||
class HookServerInstance(ThreadingHTTPServer):
|
||||
"""Custom HTTPServer that carries a reference to the main App instance."""
|
||||
def __init__(self, server_address: tuple[str, int], RequestHandlerClass: type, app: Any) -> None:
|
||||
@@ -28,14 +48,19 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
flat = project_manager.flat_config(app.project)
|
||||
flat = project_manager.flat_config(_get_app_attr(app, 'project'))
|
||||
self.wfile.write(json.dumps({'project': flat}).encode('utf-8'))
|
||||
elif self.path == '/api/session':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
with app._disc_entries_lock:
|
||||
entries_snapshot = list(app.disc_entries)
|
||||
lock = _get_app_attr(app, '_disc_entries_lock')
|
||||
entries = _get_app_attr(app, 'disc_entries', [])
|
||||
if lock:
|
||||
with lock:
|
||||
entries_snapshot = list(entries)
|
||||
else:
|
||||
entries_snapshot = list(entries)
|
||||
self.wfile.write(
|
||||
json.dumps({'session': {'entries': entries_snapshot}}).
|
||||
encode('utf-8'))
|
||||
@@ -44,8 +69,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
metrics = {}
|
||||
if hasattr(app, 'perf_monitor'):
|
||||
metrics = app.perf_monitor.get_metrics()
|
||||
perf = _get_app_attr(app, 'perf_monitor')
|
||||
if perf:
|
||||
metrics = perf.get_metrics()
|
||||
self.wfile.write(json.dumps({'performance': metrics}).encode('utf-8'))
|
||||
elif self.path == '/api/events':
|
||||
# Long-poll or return current event queue
|
||||
@@ -53,10 +79,16 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
events = []
|
||||
if hasattr(app, '_api_event_queue'):
|
||||
with app._api_event_queue_lock:
|
||||
events = list(app._api_event_queue)
|
||||
app._api_event_queue.clear()
|
||||
if _has_app_attr(app, '_api_event_queue'):
|
||||
lock = _get_app_attr(app, '_api_event_queue_lock')
|
||||
queue = _get_app_attr(app, '_api_event_queue')
|
||||
if lock:
|
||||
with lock:
|
||||
events = list(queue)
|
||||
queue.clear()
|
||||
else:
|
||||
events = list(queue)
|
||||
queue.clear()
|
||||
self.wfile.write(json.dumps({'events': events}).encode('utf-8'))
|
||||
elif self.path == '/api/gui/value':
|
||||
# POST with {"field": "field_tag"} to get value
|
||||
@@ -69,17 +101,20 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def get_val():
|
||||
try:
|
||||
if field_tag in app._settable_fields:
|
||||
attr = app._settable_fields[field_tag]
|
||||
val = getattr(app, attr, None)
|
||||
result["value"] = val
|
||||
settable = _get_app_attr(app, '_settable_fields', {})
|
||||
if field_tag in settable:
|
||||
attr = settable[field_tag]
|
||||
result["value"] = _get_app_attr(app, attr, None)
|
||||
finally:
|
||||
event.set()
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_val
|
||||
})
|
||||
lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if lock and tasks is not None:
|
||||
with lock:
|
||||
tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_val
|
||||
})
|
||||
if event.wait(timeout=60):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
@@ -96,16 +131,20 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def get_val():
|
||||
try:
|
||||
if field_tag in app._settable_fields:
|
||||
attr = app._settable_fields[field_tag]
|
||||
result["value"] = getattr(app, attr, None)
|
||||
settable = _get_app_attr(app, '_settable_fields', {})
|
||||
if field_tag in settable:
|
||||
attr = settable[field_tag]
|
||||
result["value"] = _get_app_attr(app, attr, None)
|
||||
finally:
|
||||
event.set()
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_val
|
||||
})
|
||||
lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if lock and tasks is not None:
|
||||
with lock:
|
||||
tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_val
|
||||
})
|
||||
if event.wait(timeout=60):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
@@ -120,30 +159,33 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def get_mma():
|
||||
try:
|
||||
result["mma_status"] = getattr(app, "mma_status", "idle")
|
||||
result["ai_status"] = getattr(app, "ai_status", "idle")
|
||||
result["active_tier"] = getattr(app, "active_tier", None)
|
||||
at = getattr(app, "active_track", None)
|
||||
result["mma_status"] = _get_app_attr(app, "mma_status", "idle")
|
||||
result["ai_status"] = _get_app_attr(app, "ai_status", "idle")
|
||||
result["active_tier"] = _get_app_attr(app, "active_tier", None)
|
||||
at = _get_app_attr(app, "active_track", None)
|
||||
result["active_track"] = at.id if hasattr(at, "id") else at
|
||||
result["active_tickets"] = getattr(app, "active_tickets", [])
|
||||
result["mma_step_mode"] = getattr(app, "mma_step_mode", False)
|
||||
result["pending_tool_approval"] = getattr(app, "_pending_ask_dialog", False)
|
||||
result["pending_script_approval"] = getattr(app, "_pending_dialog", None) is not None
|
||||
result["pending_mma_step_approval"] = getattr(app, "_pending_mma_approval", None) is not None
|
||||
result["pending_mma_spawn_approval"] = getattr(app, "_pending_mma_spawn", None) is not None
|
||||
result["active_tickets"] = _get_app_attr(app, "active_tickets", [])
|
||||
result["mma_step_mode"] = _get_app_attr(app, "mma_step_mode", False)
|
||||
result["pending_tool_approval"] = _get_app_attr(app, "_pending_ask_dialog", False)
|
||||
result["pending_script_approval"] = _get_app_attr(app, "_pending_dialog", None) is not None
|
||||
result["pending_mma_step_approval"] = _get_app_attr(app, "_pending_mma_approval", None) is not None
|
||||
result["pending_mma_spawn_approval"] = _get_app_attr(app, "_pending_mma_spawn", None) is not None
|
||||
result["pending_approval"] = result["pending_mma_step_approval"] or result["pending_tool_approval"]
|
||||
result["pending_spawn"] = result["pending_mma_spawn_approval"]
|
||||
result["tracks"] = getattr(app, "tracks", [])
|
||||
result["proposed_tracks"] = getattr(app, "proposed_tracks", [])
|
||||
result["mma_streams"] = getattr(app, "mma_streams", {})
|
||||
result["mma_tier_usage"] = getattr(app, "mma_tier_usage", {})
|
||||
result["tracks"] = _get_app_attr(app, "tracks", [])
|
||||
result["proposed_tracks"] = _get_app_attr(app, "proposed_tracks", [])
|
||||
result["mma_streams"] = _get_app_attr(app, "mma_streams", {})
|
||||
result["mma_tier_usage"] = _get_app_attr(app, "mma_tier_usage", {})
|
||||
finally:
|
||||
event.set()
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_mma
|
||||
})
|
||||
lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if lock and tasks is not None:
|
||||
with lock:
|
||||
tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": get_mma
|
||||
})
|
||||
if event.wait(timeout=60):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
@@ -158,17 +200,20 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def check_all():
|
||||
try:
|
||||
status = getattr(app, "ai_status", "idle")
|
||||
status = _get_app_attr(app, "ai_status", "idle")
|
||||
result["thinking"] = status in ["sending...", "running powershell..."]
|
||||
result["live"] = status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."]
|
||||
result["prior"] = getattr(app, "is_viewing_prior_session", False)
|
||||
result["prior"] = _get_app_attr(app, "is_viewing_prior_session", False)
|
||||
finally:
|
||||
event.set()
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": check_all
|
||||
})
|
||||
lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if lock and tasks is not None:
|
||||
with lock:
|
||||
tasks.append({
|
||||
"action": "custom_callback",
|
||||
"callback": check_all
|
||||
})
|
||||
if event.wait(timeout=60):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
@@ -191,7 +236,8 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
data = json.loads(body_str) if body_str else {}
|
||||
if self.path == '/api/project':
|
||||
app.project = data.get('project', app.project)
|
||||
project = _get_app_attr(app, 'project')
|
||||
_set_app_attr(app, 'project', data.get('project', project))
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
@@ -199,8 +245,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
elif self.path.startswith('/api/confirm/'):
|
||||
action_id = self.path.split('/')[-1]
|
||||
approved = data.get('approved', False)
|
||||
if hasattr(app, 'resolve_pending_action'):
|
||||
success = app.resolve_pending_action(action_id, approved)
|
||||
resolve_func = _get_app_attr(app, 'resolve_pending_action')
|
||||
if resolve_func:
|
||||
success = resolve_func(action_id, approved)
|
||||
if success:
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
@@ -213,15 +260,24 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
elif self.path == '/api/session':
|
||||
with app._disc_entries_lock:
|
||||
app.disc_entries = data.get('session', {}).get('entries', app.disc_entries)
|
||||
lock = _get_app_attr(app, '_disc_entries_lock')
|
||||
entries = _get_app_attr(app, 'disc_entries')
|
||||
new_entries = data.get('session', {}).get('entries', entries)
|
||||
if lock:
|
||||
with lock:
|
||||
_set_app_attr(app, 'disc_entries', new_entries)
|
||||
else:
|
||||
_set_app_attr(app, 'disc_entries', new_entries)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'status': 'updated'}).encode('utf-8'))
|
||||
elif self.path == '/api/gui':
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append(data)
|
||||
lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if lock and tasks is not None:
|
||||
with lock:
|
||||
tasks.append(data)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
@@ -229,35 +285,65 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
elif self.path == '/api/ask':
|
||||
request_id = str(uuid.uuid4())
|
||||
event = threading.Event()
|
||||
if not hasattr(app, '_pending_asks'): app._pending_asks = {}
|
||||
if not hasattr(app, '_ask_responses'): app._ask_responses = {}
|
||||
app._pending_asks[request_id] = event
|
||||
with app._api_event_queue_lock:
|
||||
app._api_event_queue.append({"type": "ask_received", "request_id": request_id, "data": data})
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({"type": "ask", "request_id": request_id, "data": data})
|
||||
pending_asks = _get_app_attr(app, '_pending_asks')
|
||||
if pending_asks is None:
|
||||
pending_asks = {}
|
||||
_set_app_attr(app, '_pending_asks', pending_asks)
|
||||
ask_responses = _get_app_attr(app, '_ask_responses')
|
||||
if ask_responses is None:
|
||||
ask_responses = {}
|
||||
_set_app_attr(app, '_ask_responses', ask_responses)
|
||||
pending_asks[request_id] = event
|
||||
|
||||
event_queue_lock = _get_app_attr(app, '_api_event_queue_lock')
|
||||
event_queue = _get_app_attr(app, '_api_event_queue')
|
||||
if event_queue is not None:
|
||||
if event_queue_lock:
|
||||
with event_queue_lock:
|
||||
event_queue.append({"type": "ask_received", "request_id": request_id, "data": data})
|
||||
else:
|
||||
event_queue.append({"type": "ask_received", "request_id": request_id, "data": data})
|
||||
|
||||
gui_tasks_lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
gui_tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if gui_tasks is not None:
|
||||
if gui_tasks_lock:
|
||||
with gui_tasks_lock:
|
||||
gui_tasks.append({"type": "ask", "request_id": request_id, "data": data})
|
||||
else:
|
||||
gui_tasks.append({"type": "ask", "request_id": request_id, "data": data})
|
||||
|
||||
if event.wait(timeout=60.0):
|
||||
response_data = app._ask_responses.get(request_id)
|
||||
if request_id in app._ask_responses: del app._ask_responses[request_id]
|
||||
response_data = ask_responses.get(request_id)
|
||||
if request_id in ask_responses: del ask_responses[request_id]
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'status': 'ok', 'response': response_data}).encode('utf-8'))
|
||||
else:
|
||||
if request_id in app._pending_asks: del app._pending_asks[request_id]
|
||||
if request_id in pending_asks: del pending_asks[request_id]
|
||||
self.send_response(504)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8'))
|
||||
elif self.path == '/api/ask/respond':
|
||||
request_id = data.get('request_id')
|
||||
response_data = data.get('response')
|
||||
if request_id and hasattr(app, '_pending_asks') and request_id in app._pending_asks:
|
||||
app._ask_responses[request_id] = response_data
|
||||
event = app._pending_asks[request_id]
|
||||
pending_asks = _get_app_attr(app, '_pending_asks')
|
||||
ask_responses = _get_app_attr(app, '_ask_responses')
|
||||
if request_id and pending_asks and request_id in pending_asks:
|
||||
ask_responses[request_id] = response_data
|
||||
event = pending_asks[request_id]
|
||||
event.set()
|
||||
del app._pending_asks[request_id]
|
||||
with app._pending_gui_tasks_lock:
|
||||
app._pending_gui_tasks.append({"action": "clear_ask", "request_id": request_id})
|
||||
del pending_asks[request_id]
|
||||
|
||||
gui_tasks_lock = _get_app_attr(app, '_pending_gui_tasks_lock')
|
||||
gui_tasks = _get_app_attr(app, '_pending_gui_tasks')
|
||||
if gui_tasks is not None:
|
||||
if gui_tasks_lock:
|
||||
with gui_tasks_lock:
|
||||
gui_tasks.append({"action": "clear_ask", "request_id": request_id})
|
||||
else:
|
||||
gui_tasks.append({"action": "clear_ask", "request_id": request_id})
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
@@ -274,8 +360,8 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8'))
|
||||
|
||||
def log_message(self, format: str, *args: Any) -> None:
|
||||
logging.info("Hook API: " + format % args)
|
||||
def log_message(self, format: str, *args: Any) -> None:
|
||||
logging.info("Hook API: " + format % args)
|
||||
|
||||
class HookServer:
|
||||
def __init__(self, app: Any, port: int = 8999) -> None:
|
||||
@@ -287,15 +373,15 @@ class HookServer:
|
||||
def start(self) -> None:
|
||||
if self.thread and self.thread.is_alive():
|
||||
return
|
||||
is_gemini_cli = getattr(self.app, 'current_provider', '') == 'gemini_cli'
|
||||
if not getattr(self.app, 'test_hooks_enabled', False) and not is_gemini_cli:
|
||||
is_gemini_cli = _get_app_attr(self.app, 'current_provider', '') == 'gemini_cli'
|
||||
if not _get_app_attr(self.app, 'test_hooks_enabled', False) and not is_gemini_cli:
|
||||
return
|
||||
if not hasattr(self.app, '_pending_gui_tasks'): self.app._pending_gui_tasks = []
|
||||
if not hasattr(self.app, '_pending_gui_tasks_lock'): self.app._pending_gui_tasks_lock = threading.Lock()
|
||||
if not hasattr(self.app, '_pending_asks'): self.app._pending_asks = {}
|
||||
if not hasattr(self.app, '_ask_responses'): self.app._ask_responses = {}
|
||||
if not hasattr(self.app, '_api_event_queue'): self.app._api_event_queue = []
|
||||
if not hasattr(self.app, '_api_event_queue_lock'): self.app._api_event_queue_lock = threading.Lock()
|
||||
if not _has_app_attr(self.app, '_pending_gui_tasks'): _set_app_attr(self.app, '_pending_gui_tasks', [])
|
||||
if not _has_app_attr(self.app, '_pending_gui_tasks_lock'): _set_app_attr(self.app, '_pending_gui_tasks_lock', threading.Lock())
|
||||
if not _has_app_attr(self.app, '_pending_asks'): _set_app_attr(self.app, '_pending_asks', {})
|
||||
if not _has_app_attr(self.app, '_ask_responses'): _set_app_attr(self.app, '_ask_responses', {})
|
||||
if not _has_app_attr(self.app, '_api_event_queue'): _set_app_attr(self.app, '_api_event_queue', [])
|
||||
if not _has_app_attr(self.app, '_api_event_queue_lock'): _set_app_attr(self.app, '_api_event_queue_lock', threading.Lock())
|
||||
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
@@ -160,6 +160,7 @@ class AppController:
|
||||
|
||||
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
|
||||
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
||||
self._api_event_queue: List[Dict[str, Any]] = []
|
||||
|
||||
# Pending dialogs state moved from App
|
||||
self._pending_dialog: Optional[ConfirmDialog] = None
|
||||
@@ -409,15 +410,15 @@ class AppController:
|
||||
self.models_thread = threading.Thread(target=do_fetch, daemon=True)
|
||||
self.models_thread.start()
|
||||
|
||||
def start_services(self):
|
||||
def start_services(self, app: Any = None):
|
||||
"""Starts background threads and async event loop."""
|
||||
self._prune_old_logs()
|
||||
self._init_ai_and_hooks()
|
||||
self._init_ai_and_hooks(app)
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self._loop_thread.start()
|
||||
|
||||
def _init_ai_and_hooks(self) -> None:
|
||||
def _init_ai_and_hooks(self, app: Any = None) -> None:
|
||||
import api_hooks
|
||||
ai_client.set_provider(self._current_provider, self._current_model)
|
||||
if self._current_provider == "gemini_cli":
|
||||
@@ -460,7 +461,7 @@ class AppController:
|
||||
'manual_approve': 'ui_manual_approve'
|
||||
}
|
||||
|
||||
self.hook_server = api_hooks.HookServer(self)
|
||||
self.hook_server = api_hooks.HookServer(app if app else self)
|
||||
self.hook_server.start()
|
||||
|
||||
def _run_event_loop(self):
|
||||
|
||||
27
src/gui_2.py
27
src/gui_2.py
@@ -104,7 +104,7 @@ class App:
|
||||
# Initialize controller and delegate state
|
||||
self.controller = AppController()
|
||||
self.controller.init_state()
|
||||
self.controller.start_services()
|
||||
self.controller.start_services(self)
|
||||
|
||||
# Aliases for controller-owned locks
|
||||
self._send_thread_lock = self.controller._send_thread_lock
|
||||
@@ -177,30 +177,6 @@ class App:
|
||||
"""UI-level wrapper for approving a pending MMA sub-agent spawn."""
|
||||
self._handle_mma_respond(approved=True)
|
||||
|
||||
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
|
||||
"""Delegates MMA approval response to the controller."""
|
||||
self.controller._handle_mma_respond(approved, payload, abort, prompt, context_md)
|
||||
|
||||
def _handle_approve_ask(self, user_data=None) -> None:
|
||||
"""Delegates tool approval to the controller."""
|
||||
self.controller._handle_approve_ask()
|
||||
|
||||
def _handle_reject_ask(self, user_data=None) -> None:
|
||||
"""Delegates tool rejection to the controller."""
|
||||
self.controller._handle_reject_ask()
|
||||
|
||||
def _handle_reset_session(self, user_data=None) -> None:
|
||||
"""Delegates session reset to the controller."""
|
||||
self.controller._handle_reset_session()
|
||||
|
||||
def _handle_md_only(self, user_data=None) -> None:
|
||||
"""Delegates 'MD Only' logic to the controller."""
|
||||
self.controller._handle_md_only()
|
||||
|
||||
def _handle_generate_send(self, user_data=None) -> None:
|
||||
"""Delegates 'Gen + Send' logic to the controller."""
|
||||
self.controller._handle_generate_send()
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name != 'controller' and hasattr(self, 'controller') and hasattr(self.controller, name):
|
||||
return getattr(self.controller, name)
|
||||
@@ -508,6 +484,7 @@ class App:
|
||||
self.perf_monitor.start_frame()
|
||||
# Process GUI task queue
|
||||
self._process_pending_gui_tasks()
|
||||
self._process_pending_history_adds()
|
||||
self._render_track_proposal_modal()
|
||||
# Auto-save (every 60s)
|
||||
now = time.time()
|
||||
|
||||
Reference in New Issue
Block a user