WIP: I HATE PYTHON
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
from typing import Any, List, Dict, Optional, Tuple, Callable
|
||||
from typing import Any, List, Dict, Optional, Tuple, Callable, Union, cast
|
||||
from pathlib import Path
|
||||
import json
|
||||
import uuid
|
||||
@@ -20,24 +20,24 @@ from pydantic import BaseModel
|
||||
from src import events
|
||||
from src import session_logger
|
||||
from src import project_manager
|
||||
from src.performance_monitor import PerformanceMonitor
|
||||
from src.models import Track, Ticket, load_config, parse_history_entries, DISC_ROLES, AGENT_TOOL_NAMES, CONFIG_PATH
|
||||
from src import performance_monitor
|
||||
from src import models
|
||||
from src.log_registry import LogRegistry
|
||||
from src.log_pruner import LogPruner
|
||||
from src.file_cache import ASTParser
|
||||
import ai_client
|
||||
import shell_runner
|
||||
import mcp_client
|
||||
import aggregate
|
||||
import orchestrator_pm
|
||||
import conductor_tech_lead
|
||||
import cost_tracker
|
||||
import multi_agent_conductor
|
||||
from src import ai_client
|
||||
from src import shell_runner
|
||||
from src import mcp_client
|
||||
from src import aggregate
|
||||
from src import orchestrator_pm
|
||||
from src import conductor_tech_lead
|
||||
from src import cost_tracker
|
||||
from src import multi_agent_conductor
|
||||
from src import theme
|
||||
from ai_client import ProviderError
|
||||
from src.ai_client import ProviderError
|
||||
|
||||
def save_config(config: dict[str, Any]) -> None:
|
||||
with open(CONFIG_PATH, "wb") as f:
|
||||
with open(models.CONFIG_PATH, "wb") as f:
|
||||
tomli_w.dump(config, f)
|
||||
|
||||
def hide_tk_root() -> Tk:
|
||||
@@ -141,12 +141,11 @@ class AppController:
|
||||
self.files: List[str] = []
|
||||
self.screenshots: List[str] = []
|
||||
|
||||
self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue()
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self.event_queue: events.SyncEventQueue = events.SyncEventQueue()
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
|
||||
self.tracks: List[Dict[str, Any]] = []
|
||||
self.active_track: Optional[Track] = None
|
||||
self.active_track: Optional[models.Track] = None
|
||||
self.active_tickets: List[Dict[str, Any]] = []
|
||||
self.mma_streams: Dict[str, str] = {}
|
||||
self.mma_status: str = "idle"
|
||||
@@ -169,7 +168,7 @@ class AppController:
|
||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
}
|
||||
|
||||
self.perf_monitor: PerformanceMonitor = PerformanceMonitor()
|
||||
self.perf_monitor: performance_monitor.PerformanceMonitor = performance_monitor.PerformanceMonitor()
|
||||
self._pending_gui_tasks: List[Dict[str, Any]] = []
|
||||
self._api_event_queue: List[Dict[str, Any]] = []
|
||||
|
||||
@@ -278,6 +277,40 @@ class AppController:
|
||||
self.prior_session_entries: List[Dict[str, Any]] = []
|
||||
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||
self.ui_manual_approve: bool = False
|
||||
|
||||
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',
|
||||
'project_main_context': 'ui_project_main_context',
|
||||
'gcli_path': 'ui_gemini_cli_path',
|
||||
'output_dir': 'ui_output_dir',
|
||||
'files_base_dir': 'ui_files_base_dir',
|
||||
'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',
|
||||
'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'
|
||||
}
|
||||
|
||||
self._gettable_fields = dict(self._settable_fields)
|
||||
self._gettable_fields.update({
|
||||
'ui_focus_agent': 'ui_focus_agent',
|
||||
'active_discussion': 'active_discussion',
|
||||
'_track_discussion_active': '_track_discussion_active'
|
||||
})
|
||||
|
||||
self._init_actions()
|
||||
|
||||
def _init_actions(self) -> None:
|
||||
@@ -302,6 +335,14 @@ class AppController:
|
||||
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file
|
||||
}
|
||||
|
||||
def _update_gcli_adapter(self, path: str) -> None:
|
||||
sys.stderr.write(f"[DEBUG] _update_gcli_adapter called with: {path}\n")
|
||||
sys.stderr.flush()
|
||||
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 _process_pending_gui_tasks(self) -> None:
|
||||
if not self._pending_gui_tasks:
|
||||
return
|
||||
@@ -336,6 +377,8 @@ class AppController:
|
||||
else:
|
||||
self.ai_response = text
|
||||
self.ai_status = payload.get("status", "done")
|
||||
sys.stderr.write(f"[DEBUG] Updated ai_status to: {self.ai_status}\n")
|
||||
sys.stderr.flush()
|
||||
self._trigger_blink = True
|
||||
if not stream_id:
|
||||
self._token_stats_dirty = True
|
||||
@@ -370,8 +413,8 @@ class AppController:
|
||||
if track_data:
|
||||
tickets = []
|
||||
for t_data in self.active_tickets:
|
||||
tickets.append(Ticket(**t_data))
|
||||
self.active_track = Track(
|
||||
tickets.append(models.Ticket(**t_data))
|
||||
self.active_track = models.Track(
|
||||
id=track_data.get("id"),
|
||||
description=track_data.get("title", ""),
|
||||
tickets=tickets
|
||||
@@ -379,17 +422,20 @@ class AppController:
|
||||
elif action == "set_value":
|
||||
item = task.get("item")
|
||||
value = task.get("value")
|
||||
sys.stderr.write(f"[DEBUG] Processing set_value: {item}={value}\n")
|
||||
sys.stderr.flush()
|
||||
if item in self._settable_fields:
|
||||
attr_name = self._settable_fields[item]
|
||||
setattr(self, attr_name, value)
|
||||
sys.stderr.write(f"[DEBUG] Set {attr_name} to {value}\n")
|
||||
sys.stderr.flush()
|
||||
if item == "gcli_path":
|
||||
if not ai_client._gemini_cli_adapter:
|
||||
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=str(value))
|
||||
else:
|
||||
ai_client._gemini_cli_adapter.binary_path = str(value)
|
||||
self._update_gcli_adapter(str(value))
|
||||
elif action == "click":
|
||||
item = task.get("item")
|
||||
user_data = task.get("user_data")
|
||||
sys.stderr.write(f"[DEBUG] Processing click: {item} (user_data={user_data})\n")
|
||||
sys.stderr.flush()
|
||||
if item == "btn_project_new_automated":
|
||||
self._cb_new_project_automated(user_data)
|
||||
elif item == "btn_mma_load_track":
|
||||
@@ -449,6 +495,9 @@ class AppController:
|
||||
if "dialog_container" in task:
|
||||
task["dialog_container"][0] = spawn_dlg
|
||||
except Exception as e:
|
||||
import traceback
|
||||
sys.stderr.write(f"[DEBUG] Error executing GUI task: {e}\n{traceback.format_exc()}\n")
|
||||
sys.stderr.flush()
|
||||
print(f"Error executing GUI task: {e}")
|
||||
|
||||
def _process_pending_history_adds(self) -> None:
|
||||
@@ -505,7 +554,7 @@ class AppController:
|
||||
|
||||
def init_state(self):
|
||||
"""Initializes the application state from configurations."""
|
||||
self.config = load_config()
|
||||
self.config = models.load_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")
|
||||
@@ -523,11 +572,11 @@ class AppController:
|
||||
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
|
||||
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
|
||||
self.disc_roles = list(disc_sec.get("roles", list(models.DISC_ROLES)))
|
||||
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 = parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
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")
|
||||
@@ -538,6 +587,7 @@ class AppController:
|
||||
self.ui_project_main_context = proj_meta.get("main_context", "")
|
||||
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_summary_only = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||
@@ -562,7 +612,7 @@ class AppController:
|
||||
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 AGENT_TOOL_NAMES}
|
||||
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.open_session(label=label)
|
||||
@@ -622,8 +672,10 @@ class AppController:
|
||||
"""Asynchronously prunes old insignificant logs on startup."""
|
||||
def run_prune() -> None:
|
||||
try:
|
||||
registry = LogRegistry("logs/log_registry.toml")
|
||||
pruner = LogPruner(registry, "logs")
|
||||
from src import log_registry
|
||||
from src import log_pruner
|
||||
registry = log_registry.LogRegistry("logs/log_registry.toml")
|
||||
pruner = log_pruner.LogPruner(registry, "logs")
|
||||
pruner.prune()
|
||||
except Exception as e:
|
||||
print(f"Error during log pruning: {e}")
|
||||
@@ -646,26 +698,28 @@ class AppController:
|
||||
self.models_thread.start()
|
||||
|
||||
def start_services(self, app: Any = None):
|
||||
"""Starts background threads and async event loop."""
|
||||
"""Starts background threads."""
|
||||
sys.stderr.write("[DEBUG] AppController.start_services called\n")
|
||||
sys.stderr.flush()
|
||||
self._prune_old_logs()
|
||||
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()
|
||||
sys.stderr.write(f"[DEBUG] _loop_thread started: {self._loop_thread.ident}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Stops background threads and cleans up resources."""
|
||||
import ai_client
|
||||
from src import ai_client
|
||||
ai_client.cleanup()
|
||||
if hasattr(self, 'hook_server') and self.hook_server:
|
||||
self.hook_server.stop()
|
||||
if self._loop and self._loop.is_running():
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
self.event_queue.put("shutdown", None)
|
||||
if self._loop_thread and self._loop_thread.is_alive():
|
||||
self._loop_thread.join(timeout=2.0)
|
||||
|
||||
def _init_ai_and_hooks(self, app: Any = None) -> None:
|
||||
import api_hooks
|
||||
from src import api_hooks
|
||||
ai_client.set_provider(self._current_provider, self._current_model)
|
||||
if self._current_provider == "gemini_cli":
|
||||
if not ai_client._gemini_cli_adapter:
|
||||
@@ -681,77 +735,37 @@ class AppController:
|
||||
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._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',
|
||||
'project_main_context': 'ui_project_main_context',
|
||||
'gcli_path': 'ui_gemini_cli_path',
|
||||
'output_dir': 'ui_output_dir',
|
||||
'files_base_dir': 'ui_files_base_dir',
|
||||
'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',
|
||||
'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'
|
||||
}
|
||||
|
||||
self._gettable_fields = dict(self._settable_fields)
|
||||
self._gettable_fields.update({
|
||||
'ui_focus_agent': 'ui_focus_agent',
|
||||
'active_discussion': 'active_discussion',
|
||||
'_track_discussion_active': '_track_discussion_active'
|
||||
})
|
||||
|
||||
self.hook_server = api_hooks.HookServer(app if app else self)
|
||||
self.hook_server.start()
|
||||
|
||||
def _run_event_loop(self):
|
||||
"""Internal loop runner."""
|
||||
asyncio.set_event_loop(self._loop)
|
||||
self._loop.create_task(self._process_event_queue())
|
||||
|
||||
# Fallback: process queues even if GUI thread is idling/stuck (or in headless mode)
|
||||
async def queue_fallback() -> None:
|
||||
def queue_fallback() -> None:
|
||||
while True:
|
||||
try:
|
||||
# These methods are normally called by the GUI thread,
|
||||
# but we call them here as a fallback for headless/background operations.
|
||||
# The methods themselves are expected to be thread-safe or handle locks.
|
||||
# Since they are on 'self' (the controller), and App delegates to them,
|
||||
# we need to make sure we don't double-process if App is also calling them.
|
||||
# However, _pending_gui_tasks uses a lock, so it's safe.
|
||||
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
|
||||
await asyncio.sleep(0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
fallback_thread = threading.Thread(target=queue_fallback, daemon=True)
|
||||
fallback_thread.start()
|
||||
self._process_event_queue()
|
||||
|
||||
self._loop.create_task(queue_fallback())
|
||||
self._loop.run_forever()
|
||||
|
||||
async def _process_event_queue(self) -> None:
|
||||
"""Listens for and processes events from the AsyncEventQueue."""
|
||||
sys.stderr.write("[DEBUG] _process_event_queue started\n")
|
||||
def _process_event_queue(self) -> None:
|
||||
"""Listens for and processes events from the SyncEventQueue."""
|
||||
sys.stderr.write("[DEBUG] _process_event_queue entered\n")
|
||||
sys.stderr.flush()
|
||||
while True:
|
||||
event_name, payload = await self.event_queue.get()
|
||||
event_name, payload = self.event_queue.get()
|
||||
sys.stderr.write(f"[DEBUG] _process_event_queue got event: {event_name}\n")
|
||||
sys.stderr.flush()
|
||||
if event_name == "shutdown":
|
||||
break
|
||||
if event_name == "user_request":
|
||||
self._loop.run_in_executor(None, self._handle_request_event, payload)
|
||||
threading.Thread(target=self._handle_request_event, args=(payload,), daemon=True).start()
|
||||
elif event_name == "response":
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({
|
||||
@@ -792,6 +806,10 @@ class AppController:
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
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)
|
||||
sys.stderr.write(f"[DEBUG] Calling ai_client.send with provider={ai_client.get_provider()}, model={self.current_model}, gcli_path={self.ui_gemini_cli_path}\n")
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
resp = ai_client.send(
|
||||
event.stable_md,
|
||||
@@ -804,27 +822,20 @@ class AppController:
|
||||
pre_tool_callback=self._confirm_and_run,
|
||||
qa_callback=ai_client.run_tier4_analysis
|
||||
)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"}),
|
||||
self._loop
|
||||
)
|
||||
except ProviderError as e:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"}),
|
||||
self._loop
|
||||
)
|
||||
self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"})
|
||||
except ai_client.ProviderError as e:
|
||||
sys.stderr.write(f"[DEBUG] _handle_request_event ai_client.ProviderError: {e.ui_message()}\n")
|
||||
sys.stderr.flush()
|
||||
self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"})
|
||||
except Exception as e:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"}),
|
||||
self._loop
|
||||
)
|
||||
import traceback
|
||||
sys.stderr.write(f"[DEBUG] _handle_request_event ERROR: {e}\n{traceback.format_exc()}\n")
|
||||
sys.stderr.flush()
|
||||
self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"})
|
||||
|
||||
def _on_ai_stream(self, text: str) -> None:
|
||||
"""Handles streaming text from the AI."""
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"}),
|
||||
self._loop
|
||||
)
|
||||
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
|
||||
|
||||
def _on_comms_entry(self, entry: Dict[str, Any]) -> None:
|
||||
session_logger.log_comms(entry)
|
||||
@@ -866,7 +877,7 @@ class AppController:
|
||||
with self._pending_tool_calls_lock:
|
||||
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
|
||||
|
||||
def _on_api_event(self, event_name: str, **kwargs: Any) -> None:
|
||||
def _on_api_event(self, event_name: str = "generic_event", **kwargs: Any) -> None:
|
||||
payload = kwargs.get("payload", {})
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({"action": "refresh_api_metrics", "payload": payload})
|
||||
@@ -1083,7 +1094,7 @@ class AppController:
|
||||
},
|
||||
"usage": self.session_usage
|
||||
}
|
||||
except ProviderError as e:
|
||||
except ai_client.ProviderError as e:
|
||||
raise HTTPException(status_code=502, detail=f"AI Provider Error: {e.ui_message()}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"In-flight AI request failure: {e}")
|
||||
@@ -1207,11 +1218,11 @@ class AppController:
|
||||
self.files = list(self.project.get("files", {}).get("paths", []))
|
||||
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
|
||||
self.disc_roles = list(disc_sec.get("roles", list(models.DISC_ROLES)))
|
||||
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 = parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
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", ".")
|
||||
@@ -1226,7 +1237,7 @@ class AppController:
|
||||
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
|
||||
self.ui_summary_only = proj.get("project", {}).get("summary_only", False)
|
||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
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.ui_files_base_dir)
|
||||
# Restore MMA state
|
||||
@@ -1237,8 +1248,8 @@ class AppController:
|
||||
try:
|
||||
tickets = []
|
||||
for t_data in at_data.get("tickets", []):
|
||||
tickets.append(Ticket(**t_data))
|
||||
self.active_track = Track(
|
||||
tickets.append(models.Ticket(**t_data))
|
||||
self.active_track = models.Track(
|
||||
id=at_data.get("id"),
|
||||
description=at_data.get("description"),
|
||||
tickets=tickets
|
||||
@@ -1255,7 +1266,7 @@ class AppController:
|
||||
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
|
||||
if track_history:
|
||||
with self._disc_entries_lock:
|
||||
self.disc_entries = parse_history_entries(track_history, self.disc_roles)
|
||||
self.disc_entries = models.parse_history_entries(track_history, self.disc_roles)
|
||||
|
||||
def _cb_load_track(self, track_id: str) -> None:
|
||||
state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
|
||||
@@ -1265,21 +1276,21 @@ class AppController:
|
||||
tickets = []
|
||||
for t in state.tasks:
|
||||
if isinstance(t, dict):
|
||||
tickets.append(Ticket(**t))
|
||||
tickets.append(models.Ticket(**t))
|
||||
else:
|
||||
tickets.append(t)
|
||||
self.active_track = Track(
|
||||
self.active_track = models.Track(
|
||||
id=state.metadata.id,
|
||||
description=state.metadata.name,
|
||||
tickets=tickets
|
||||
)
|
||||
# Keep dicts for UI table (or convert Ticket objects back to dicts if needed)
|
||||
# Keep dicts for UI table (or convert models.Ticket objects back to dicts if needed)
|
||||
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in tickets]
|
||||
# Load track-scoped history
|
||||
history = project_manager.load_track_history(track_id, self.ui_files_base_dir)
|
||||
with self._disc_entries_lock:
|
||||
if history:
|
||||
self.disc_entries = parse_history_entries(history, self.disc_roles)
|
||||
self.disc_entries = models.parse_history_entries(history, self.disc_roles)
|
||||
else:
|
||||
self.disc_entries = []
|
||||
self._recalculate_session_usage()
|
||||
@@ -1312,7 +1323,7 @@ class AppController:
|
||||
disc_sec["active"] = name
|
||||
disc_data = discussions[name]
|
||||
with self._disc_entries_lock:
|
||||
self.disc_entries = parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
self.disc_entries = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
self.ai_status = f"discussion: {name}"
|
||||
|
||||
def _flush_disc_entries_to_project(self) -> None:
|
||||
@@ -1491,10 +1502,7 @@ class AppController:
|
||||
base_dir=base_dir
|
||||
)
|
||||
# Push to async queue
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("user_request", event_payload),
|
||||
self._loop
|
||||
)
|
||||
self.event_queue.put("user_request", event_payload)
|
||||
sys.stderr.write("[DEBUG] Enqueued user_request event\n")
|
||||
sys.stderr.flush()
|
||||
except Exception as e:
|
||||
@@ -1549,7 +1557,7 @@ class AppController:
|
||||
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 AGENT_TOOL_NAMES:
|
||||
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", {})
|
||||
@@ -1598,9 +1606,13 @@ class AppController:
|
||||
|
||||
def _cb_plan_epic(self) -> None:
|
||||
def _bg_task() -> None:
|
||||
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
|
||||
sys.stderr.flush()
|
||||
try:
|
||||
self.ai_status = "Planning Epic (Tier 1)..."
|
||||
history = orchestrator_pm.get_track_history_summary()
|
||||
sys.stderr.write(f"[DEBUG] History summary length: {len(history)}\n")
|
||||
sys.stderr.flush()
|
||||
proj = project_manager.load_project(self.active_project_path)
|
||||
flat = project_manager.flat_config(proj)
|
||||
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
|
||||
@@ -1641,7 +1653,7 @@ class AppController:
|
||||
def _bg_task() -> None:
|
||||
# Generate skeletons once
|
||||
self.ai_status = "Phase 2: Generating skeletons for all tracks..."
|
||||
parser = ASTParser(language="python")
|
||||
parser = file_cache.ASTParser(language="python")
|
||||
generated_skeletons = ""
|
||||
try:
|
||||
for i, file_path in enumerate(self.files):
|
||||
@@ -1685,7 +1697,7 @@ class AppController:
|
||||
engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode)
|
||||
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id)
|
||||
full_md, _, _ = aggregate.run(flat)
|
||||
asyncio.run_coroutine_threadsafe(engine.run(md_content=full_md), self._loop)
|
||||
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
|
||||
self.ai_status = f"Track '{self.active_track.description}' started."
|
||||
return
|
||||
|
||||
@@ -1709,7 +1721,7 @@ class AppController:
|
||||
skeletons = "" # Initialize skeletons variable
|
||||
if skeletons_str is None: # Only generate if not provided
|
||||
# 1. Get skeletons for context
|
||||
parser = ASTParser(language="python")
|
||||
parser = file_cache.ASTParser(language="python")
|
||||
for i, file_path in enumerate(self.files):
|
||||
try:
|
||||
self.ai_status = f"Phase 2: Scanning files ({i+1}/{len(self.files)})..."
|
||||
@@ -1745,7 +1757,7 @@ class AppController:
|
||||
# 3. Create Track and Ticket objects
|
||||
tickets = []
|
||||
for t_data in sorted_tickets_data:
|
||||
ticket = Ticket(
|
||||
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"),
|
||||
@@ -1755,11 +1767,10 @@ class AppController:
|
||||
)
|
||||
tickets.append(ticket)
|
||||
track_id = f"track_{uuid.uuid5(uuid.NAMESPACE_DNS, f'{self.active_project_path}_{title}').hex[:12]}"
|
||||
track = Track(id=track_id, description=title, tickets=tickets)
|
||||
track = models.Track(id=track_id, description=title, tickets=tickets)
|
||||
# Initialize track state in the filesystem
|
||||
from src.models import TrackState, Metadata
|
||||
meta = Metadata(id=track_id, name=title, status="todo", created_at=datetime.now(), updated_at=datetime.now())
|
||||
state = TrackState(metadata=meta, discussion=[], tasks=tickets)
|
||||
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.ui_files_base_dir)
|
||||
# 4. Initialize ConductorEngine and run loop
|
||||
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode)
|
||||
@@ -1767,8 +1778,8 @@ class AppController:
|
||||
track_id_param = track.id
|
||||
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param)
|
||||
full_md, _, _ = aggregate.run(flat)
|
||||
# Schedule the coroutine on the internal event loop
|
||||
asyncio.run_coroutine_threadsafe(engine.run(md_content=full_md), self._loop)
|
||||
# Start the engine in a separate thread
|
||||
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
|
||||
except Exception as e:
|
||||
self.ai_status = f"Track start error: {e}"
|
||||
print(f"ERROR in _start_track_logic: {e}")
|
||||
@@ -1778,20 +1789,14 @@ class AppController:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'todo'
|
||||
break
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}),
|
||||
self._loop
|
||||
)
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
|
||||
|
||||
def _cb_ticket_skip(self, ticket_id: str) -> None:
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'skipped'
|
||||
break
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("mma_skip", {"ticket_id": ticket_id}),
|
||||
self._loop
|
||||
)
|
||||
self.event_queue.put("mma_skip", {"ticket_id": ticket_id})
|
||||
|
||||
def _cb_run_conductor_setup(self) -> None:
|
||||
base = Path("conductor")
|
||||
@@ -1848,23 +1853,21 @@ class AppController:
|
||||
def _push_mma_state_update(self) -> None:
|
||||
if not self.active_track:
|
||||
return
|
||||
# Sync active_tickets (list of dicts) back to active_track.tickets (list of Ticket objects)
|
||||
self.active_track.tickets = [Ticket.from_dict(t) for t in self.active_tickets]
|
||||
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects)
|
||||
self.active_track.tickets = [models.Ticket.from_dict(t) for t in self.active_tickets]
|
||||
# Save the state to disk
|
||||
from src.project_manager import save_track_state, load_track_state
|
||||
from src.models import TrackState, Metadata
|
||||
|
||||
existing = load_track_state(self.active_track.id, self.ui_files_base_dir)
|
||||
meta = Metadata(
|
||||
existing = project_manager.load_track_state(self.active_track.id, self.ui_files_base_dir)
|
||||
meta = models.Metadata(
|
||||
id=self.active_track.id,
|
||||
name=self.active_track.description,
|
||||
status=self.mma_status,
|
||||
created_at=existing.metadata.created_at if existing else datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
state = TrackState(
|
||||
state = models.TrackState(
|
||||
metadata=meta,
|
||||
discussion=existing.discussion if existing else [],
|
||||
tasks=self.active_track.tickets
|
||||
)
|
||||
save_track_state(self.active_track.id, state, self.ui_files_base_dir)
|
||||
project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir)
|
||||
|
||||
Reference in New Issue
Block a user