fix(logging): Update GUI and controller to use correct session log paths and fix syntax errors.
This commit is contained in:
@@ -116,7 +116,7 @@ class AppController:
|
||||
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek"]
|
||||
|
||||
def __init__(self):
|
||||
# Initialize locks first to avoid initialization order issues
|
||||
# Initialize locks first to avoid initialization order issues
|
||||
self._send_thread_lock: threading.Lock = threading.Lock()
|
||||
self._disc_entries_lock: threading.Lock = threading.Lock()
|
||||
self._pending_comms_lock: threading.Lock = threading.Lock()
|
||||
@@ -125,7 +125,6 @@ class AppController:
|
||||
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.config: Dict[str, Any] = {}
|
||||
self.project: Dict[str, Any] = {}
|
||||
self.active_project_path: str = ""
|
||||
@@ -135,19 +134,15 @@ class AppController:
|
||||
self.disc_roles: List[str] = []
|
||||
self.files: List[str] = []
|
||||
self.screenshots: List[str] = []
|
||||
|
||||
self.event_queue: events.SyncEventQueue = events.SyncEventQueue()
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
|
||||
self.tracks: List[Dict[str, Any]] = []
|
||||
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"
|
||||
|
||||
self._tool_log: List[Dict[str, Any]] = []
|
||||
self._comms_log: List[Dict[str, Any]] = []
|
||||
|
||||
self.session_usage: Dict[str, Any] = {
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
@@ -155,31 +150,26 @@ class AppController:
|
||||
"cache_creation_input_tokens": 0,
|
||||
"last_latency": 0.0
|
||||
}
|
||||
|
||||
self.mma_tier_usage: Dict[str, Dict[str, Any]] = {
|
||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||
"Tier 2": {"input": 0, "output": 0, "model": "gemini-3-flash-preview"},
|
||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||
}
|
||||
|
||||
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]] = []
|
||||
|
||||
# Pending dialogs state moved from App
|
||||
self._pending_dialog: Optional[ConfirmDialog] = None
|
||||
self._pending_dialog_open: bool = False
|
||||
self._pending_actions: Dict[str, ConfirmDialog] = {}
|
||||
self._pending_ask_dialog: bool = False
|
||||
|
||||
# AI settings state
|
||||
self._current_provider: str = "gemini"
|
||||
self._current_model: str = "gemini-2.5-flash-lite"
|
||||
self.temperature: float = 0.0
|
||||
self.max_tokens: int = 8192
|
||||
self.history_trunc_limit: int = 8000
|
||||
|
||||
# UI-related state moved to controller
|
||||
self.ui_ai_input: str = ""
|
||||
self.ui_disc_new_name_input: str = ""
|
||||
@@ -195,7 +185,6 @@ class AppController:
|
||||
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 = ""
|
||||
@@ -208,7 +197,6 @@ class AppController:
|
||||
self.ui_auto_add_history: bool = False
|
||||
self.ui_global_system_prompt: str = ""
|
||||
self.ui_agent_tools: Dict[str, bool] = {}
|
||||
|
||||
self.available_models: List[str] = []
|
||||
self.proposed_tracks: List[Dict[str, Any]] = []
|
||||
self._show_track_proposal_modal: bool = False
|
||||
@@ -231,7 +219,6 @@ class AppController:
|
||||
self._perf_last_update: float = 0.0
|
||||
self._autosave_interval: float = 60.0
|
||||
self._last_autosave: float = time.time()
|
||||
|
||||
# More state moved from App
|
||||
self._ask_dialog_open: bool = False
|
||||
self._ask_request_id: Optional[str] = None
|
||||
@@ -248,7 +235,6 @@ class AppController:
|
||||
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
|
||||
@@ -272,7 +258,6 @@ 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',
|
||||
@@ -298,22 +283,20 @@ class AppController:
|
||||
'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',
|
||||
'proposed_tracks': 'proposed_tracks',
|
||||
'mma_streams': 'mma_streams',
|
||||
'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'
|
||||
})
|
||||
|
||||
'ui_focus_agent': 'ui_focus_agent',
|
||||
'active_discussion': 'active_discussion',
|
||||
'_track_discussion_active': '_track_discussion_active',
|
||||
'proposed_tracks': 'proposed_tracks',
|
||||
'mma_streams': 'mma_streams',
|
||||
'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'
|
||||
})
|
||||
self._init_actions()
|
||||
|
||||
@property
|
||||
@@ -329,7 +312,7 @@ class AppController:
|
||||
return self.is_viewing_prior_session
|
||||
|
||||
def _init_actions(self) -> None:
|
||||
# Set up state-related action maps
|
||||
# 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,
|
||||
@@ -350,6 +333,7 @@ class AppController:
|
||||
'_test_callback_func_write_to_file': self._test_callback_func_write_to_file,
|
||||
'_set_env_var': lambda k, v: os.environ.update({k: v})
|
||||
}
|
||||
|
||||
def _update_gcli_adapter(self, path: str) -> None:
|
||||
sys.stderr.write(f"[DEBUG] _update_gcli_adapter called with: {path}\n")
|
||||
sys.stderr.flush()
|
||||
@@ -385,7 +369,7 @@ class AppController:
|
||||
action = task.get("action")
|
||||
if action:
|
||||
session_logger.log_api_hook("PROCESS_TASK", action, str(task))
|
||||
# ...
|
||||
# ...
|
||||
if action == "refresh_api_metrics":
|
||||
self._refresh_api_metrics(task.get("payload", {}), md_content=self.last_md or None)
|
||||
elif action == "set_ai_status":
|
||||
@@ -419,7 +403,7 @@ class AppController:
|
||||
self._trigger_blink = True
|
||||
if not stream_id:
|
||||
self._token_stats_dirty = True
|
||||
# ONLY add to history when turn is complete
|
||||
# ONLY add to history when turn is complete
|
||||
if self.ui_auto_add_history and not stream_id and not is_streaming:
|
||||
role = payload.get("role", "AI")
|
||||
with self._pending_history_adds_lock:
|
||||
@@ -430,7 +414,7 @@ class AppController:
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
elif action in ("mma_stream", "mma_stream_append"):
|
||||
# Some events might have these at top level, some in a 'payload' dict
|
||||
# Some events might have these at top level, some in a 'payload' dict
|
||||
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:
|
||||
@@ -441,11 +425,10 @@ class AppController:
|
||||
self.proposed_tracks = task.get("payload", [])
|
||||
self._show_track_proposal_modal = True
|
||||
elif action == "mma_state_update":
|
||||
# Handle both internal (nested) and hook-server (flattened) payloads
|
||||
# Handle both internal (nested) and hook-server (flattened) payloads
|
||||
payload = task.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
payload = task # Fallback to task if payload missing or wrong type
|
||||
|
||||
self.mma_status = payload.get("status", "idle")
|
||||
self.active_tier = payload.get("active_tier")
|
||||
self.mma_tier_usage = payload.get("tier_usage", self.mma_tier_usage)
|
||||
@@ -604,24 +587,19 @@ class AppController:
|
||||
self.temperature = ai_cfg.get("temperature", 0.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()
|
||||
|
||||
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(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 = models.parse_history_entries(disc_data.get("history", []), self.disc_roles)
|
||||
|
||||
# UI state
|
||||
# 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", ".")
|
||||
@@ -635,7 +613,6 @@ class AppController:
|
||||
self.ui_summary_only = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||
|
||||
_default_windows = {
|
||||
"Context Hub": True,
|
||||
"Files & Media": True,
|
||||
@@ -653,10 +630,8 @@ class AppController:
|
||||
}
|
||||
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.open_session(label=label)
|
||||
|
||||
@@ -664,7 +639,7 @@ class AppController:
|
||||
root = hide_tk_root()
|
||||
path = filedialog.askopenfilename(
|
||||
title="Load Session Log",
|
||||
initialdir="logs",
|
||||
initialdir="logs/sessions",
|
||||
filetypes=[("Log/JSONL", "*.log *.jsonl"), ("All Files", "*.*")]
|
||||
)
|
||||
root.destroy()
|
||||
@@ -713,20 +688,21 @@ class AppController:
|
||||
|
||||
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("logs/log_registry.toml")
|
||||
pruner = log_pruner.LogPruner(registry, "logs")
|
||||
pruner.prune()
|
||||
except Exception as e:
|
||||
registry = log_registry.LogRegistry("logs/sessions/log_registry.toml")
|
||||
pruner = log_pruner.LogPruner(registry, "logs/sessions")
|
||||
pruner.prune() except Exception as e:
|
||||
print(f"Error during log pruning: {e}")
|
||||
thread = threading.Thread(target=run_prune, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _fetch_models(self, provider: str) -> None:
|
||||
self._set_status("fetching models...")
|
||||
|
||||
def do_fetch() -> None:
|
||||
try:
|
||||
models_list = ai_client.list_models(provider)
|
||||
@@ -777,12 +753,12 @@ class AppController:
|
||||
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:
|
||||
@@ -792,7 +768,6 @@ class AppController:
|
||||
self._process_pending_history_adds()
|
||||
except: pass
|
||||
time.sleep(0.1)
|
||||
|
||||
fallback_thread = threading.Thread(target=queue_fallback, daemon=True)
|
||||
fallback_thread.start()
|
||||
self._process_event_queue()
|
||||
@@ -801,16 +776,14 @@ class AppController:
|
||||
"""Listens for and processes events from the SyncEventQueue."""
|
||||
sys.stderr.write("[DEBUG] _process_event_queue entered\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def tick_perf():
|
||||
while True:
|
||||
self.perf_monitor.start_frame()
|
||||
time.sleep(0.01) # Measurable frame time
|
||||
self.perf_monitor.end_frame()
|
||||
time.sleep(0.006) # Aim for ~60 FPS total
|
||||
|
||||
threading.Thread(target=tick_perf, daemon=True).start()
|
||||
|
||||
while True:
|
||||
event_name, payload = self.event_queue.get()
|
||||
sys.stderr.write(f"[DEBUG] _process_event_queue got event: {event_name} with payload: {str(payload)[:100]}\n")
|
||||
@@ -821,8 +794,8 @@ class AppController:
|
||||
threading.Thread(target=self._handle_request_event, args=(payload,), daemon=True).start()
|
||||
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.
|
||||
# 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:
|
||||
@@ -838,7 +811,7 @@ class AppController:
|
||||
})
|
||||
elif event_name in ("mma_spawn_approval", "mma_step_approval"):
|
||||
with self._pending_gui_tasks_lock:
|
||||
# These payloads already contain the 'action' field
|
||||
# These payloads already contain the 'action' field
|
||||
self._pending_gui_tasks.append(payload)
|
||||
elif event_name == "response":
|
||||
with self._pending_gui_tasks_lock:
|
||||
@@ -849,6 +822,7 @@ class AppController:
|
||||
if self.test_hooks_enabled:
|
||||
with self._api_event_queue_lock:
|
||||
self._api_event_queue.append({"type": "response", "payload": payload})
|
||||
|
||||
def _handle_request_event(self, event: events.UserRequestEvent) -> None:
|
||||
"""Processes a UserRequestEvent by calling the AI client."""
|
||||
ai_client.set_current_tier(None) # Ensure main discussion is untagged
|
||||
@@ -860,10 +834,8 @@ class AppController:
|
||||
"collapsed": False,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
|
||||
# Clear response area for new turn
|
||||
# Clear response area for new turn
|
||||
self.ai_response = ""
|
||||
|
||||
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_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
@@ -946,6 +918,7 @@ class AppController:
|
||||
if self.test_hooks_enabled:
|
||||
with self._api_event_queue_lock:
|
||||
self._api_event_queue.append({"type": event_name, "payload": payload})
|
||||
|
||||
def _on_performance_alert(self, message: str) -> None:
|
||||
alert_text = f"[PERFORMANCE ALERT] {message}. Please consider optimizing recent changes or reducing load."
|
||||
with self._pending_history_adds_lock:
|
||||
@@ -966,7 +939,6 @@ class AppController:
|
||||
self._append_tool_log(script, output)
|
||||
self._set_status("powershell done, awaiting AI...")
|
||||
return output
|
||||
|
||||
sys.stderr.write("[DEBUG] Creating ConfirmDialog.\n")
|
||||
sys.stderr.flush()
|
||||
dialog = ConfirmDialog(script, base_dir)
|
||||
@@ -977,7 +949,6 @@ class AppController:
|
||||
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({
|
||||
@@ -989,7 +960,6 @@ class AppController:
|
||||
})
|
||||
sys.stderr.write(f"[DEBUG] Appended script_confirmation_required to _api_event_queue. ID={dialog._uid}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
sys.stderr.write(f"[DEBUG] Waiting for dialog ID={dialog._uid}...\n")
|
||||
sys.stderr.flush()
|
||||
approved, final_script = dialog.wait()
|
||||
@@ -1034,6 +1004,7 @@ class AppController:
|
||||
dialog._condition.notify_all()
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_provider(self) -> str:
|
||||
return self._current_provider
|
||||
@@ -1063,7 +1034,6 @@ class AppController:
|
||||
def create_api(self) -> FastAPI:
|
||||
"""Creates and configures the FastAPI application for headless mode."""
|
||||
api = FastAPI(title="Manual Slop Headless API")
|
||||
|
||||
API_KEY_NAME = "X-API-KEY"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
@@ -1244,27 +1214,28 @@ class AppController:
|
||||
|
||||
@api.get("/api/v1/sessions", dependencies=[Depends(get_api_key)])
|
||||
def list_sessions() -> list[str]:
|
||||
"""Lists all session log files."""
|
||||
log_dir = Path("logs")
|
||||
"""Lists all session IDs."""
|
||||
log_dir = Path("logs/sessions")
|
||||
if not log_dir.exists():
|
||||
return []
|
||||
return [f.name for f in log_dir.glob("*.log")]
|
||||
return [d.name for d in log_dir.iterdir() if d.is_dir()]
|
||||
|
||||
@api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
|
||||
def get_session(session_id: str) -> dict[str, Any]:
|
||||
"""Returns the content of a specific session log."""
|
||||
log_path = Path("logs") / session_id
|
||||
"""Returns the content of the comms.log for a specific session."""
|
||||
log_path = Path("logs/sessions") / 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")}
|
||||
|
||||
@api.delete("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
|
||||
def delete_session(session_id: str) -> dict[str, str]:
|
||||
"""Deletes a specific session log."""
|
||||
log_path = Path("logs") / session_id
|
||||
if not log_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Session log not found")
|
||||
log_path.unlink()
|
||||
"""Deletes a specific session directory."""
|
||||
log_path = Path("logs/sessions") / 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"}
|
||||
|
||||
@api.get("/api/v1/context", dependencies=[Depends(get_api_key)])
|
||||
@@ -1288,7 +1259,6 @@ class AppController:
|
||||
def token_stats() -> dict[str, Any]:
|
||||
"""Returns current token usage and budget statistics."""
|
||||
return self._token_stats
|
||||
|
||||
return api
|
||||
|
||||
def _cb_new_project_automated(self, user_data: Any) -> None:
|
||||
@@ -1327,6 +1297,7 @@ class AppController:
|
||||
return
|
||||
self._refresh_from_project()
|
||||
self._set_status(f"switched to: {Path(path).stem}")
|
||||
|
||||
def _refresh_from_project(self) -> None:
|
||||
self.files = list(self.project.get("files", {}).get("paths", []))
|
||||
self.screenshots = list(self.project.get("screenshots", {}).get("paths", []))
|
||||
@@ -1559,7 +1530,6 @@ class AppController:
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
for d_name in discussions:
|
||||
discussions[d_name]["history"] = []
|
||||
|
||||
self._set_status("session reset")
|
||||
self.ai_response = ""
|
||||
self.ui_ai_input = ""
|
||||
@@ -1568,7 +1538,6 @@ class AppController:
|
||||
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:
|
||||
@@ -1578,6 +1547,7 @@ class AppController:
|
||||
|
||||
def _handle_md_only(self) -> None:
|
||||
"""Logic for the 'MD Only' action."""
|
||||
|
||||
def worker():
|
||||
try:
|
||||
md, path, *_ = self._do_generate()
|
||||
@@ -1592,6 +1562,7 @@ class AppController:
|
||||
|
||||
def _handle_generate_send(self) -> None:
|
||||
"""Logic for the 'Gen + Send' action."""
|
||||
|
||||
def worker():
|
||||
sys.stderr.write("[DEBUG] _handle_generate_send worker started\n")
|
||||
sys.stderr.flush()
|
||||
@@ -1601,7 +1572,6 @@ class AppController:
|
||||
self.last_md = md
|
||||
self.last_md_path = path
|
||||
self.last_file_items = file_items
|
||||
|
||||
self._set_status("sending...")
|
||||
user_msg = self.ui_ai_input
|
||||
base_dir = self.ui_files_base_dir
|
||||
@@ -1640,14 +1610,12 @@ class AppController:
|
||||
if "latency" in payload:
|
||||
self.session_usage["last_latency"] = payload["latency"]
|
||||
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)
|
||||
@@ -1736,6 +1704,7 @@ class AppController:
|
||||
_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
|
||||
@@ -1764,16 +1733,16 @@ class AppController:
|
||||
|
||||
def _cb_accept_tracks(self) -> None:
|
||||
self._show_track_proposal_modal = False
|
||||
|
||||
def _bg_task() -> None:
|
||||
sys.stderr.write("[DEBUG] _cb_accept_tracks _bg_task started\n")
|
||||
# Generate skeletons once
|
||||
self._set_status("Phase 2: Generating skeletons for all tracks...")
|
||||
sys.stderr.write("[DEBUG] Creating ASTParser...\n")
|
||||
parser = ASTParser(language="python")
|
||||
|
||||
generated_skeletons = ""
|
||||
try:
|
||||
# Use a local copy of files to avoid concurrent modification issues
|
||||
# Use a local copy of files to avoid concurrent modification issues
|
||||
files_to_scan = list(self.files)
|
||||
sys.stderr.write(f"[DEBUG] Scanning {len(files_to_scan)} files for skeletons...\n")
|
||||
for i, file_path in enumerate(files_to_scan):
|
||||
@@ -1790,7 +1759,6 @@ class AppController:
|
||||
sys.stderr.write(f"[DEBUG] Error in scan loop: {e}\n")
|
||||
self._set_status(f"Error generating skeletons: {e}")
|
||||
return # Exit if skeleton generation fails
|
||||
|
||||
sys.stderr.write("[DEBUG] Skeleton generation complete. Starting tracks...\n")
|
||||
# Now loop through tracks and call _start_track_logic with generated skeletons
|
||||
total_tracks = len(self.proposed_tracks)
|
||||
@@ -1798,7 +1766,6 @@ class AppController:
|
||||
title = track_data.get("title") or track_data.get("goal", "Untitled Track")
|
||||
self._set_status(f"Processing track {i+1} of {total_tracks}: '{title}'...")
|
||||
self._start_track_logic(track_data, skeletons_str=generated_skeletons) # Pass skeletons
|
||||
|
||||
sys.stderr.write("[DEBUG] All tracks started. Refreshing...\n")
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({'action': 'refresh_from_project'}) # Ensure UI refresh after tracks are started
|
||||
@@ -1807,14 +1774,13 @@ class AppController:
|
||||
|
||||
def _cb_start_track(self, user_data: Any = None) -> None:
|
||||
if isinstance(user_data, str):
|
||||
# If track_id is provided directly
|
||||
# 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:
|
||||
# Use the active track object directly to start execution
|
||||
# Use the active track object directly to start execution
|
||||
self._set_mma_status("running")
|
||||
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)
|
||||
@@ -1822,7 +1788,6 @@ class AppController:
|
||||
threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start()
|
||||
self._set_status(f"Track '{self.active_track.description}' started.")
|
||||
return
|
||||
|
||||
idx = 0
|
||||
if isinstance(user_data, int):
|
||||
idx = user_data
|
||||
@@ -1839,9 +1804,7 @@ class AppController:
|
||||
goal = track_data.get("goal", "")
|
||||
title = track_data.get("title") or track_data.get("goal", "Untitled Track")
|
||||
self._set_status(f"Phase 2: Generating tickets for {title}...")
|
||||
|
||||
skeletons = skeletons_str or "" # Use provided skeletons or empty
|
||||
|
||||
self._set_status("Phase 2: Calling Tech Lead...")
|
||||
_t2_baseline = len(ai_client.get_comms_log())
|
||||
raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
|
||||
@@ -1849,18 +1812,16 @@ class AppController:
|
||||
_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._set_status(f"Error: No tickets generated for track: {title}")
|
||||
print(f"Warning: No tickets generated for track: {title}")
|
||||
@@ -1889,13 +1850,11 @@ class AppController:
|
||||
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)
|
||||
|
||||
# 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
|
||||
# 4. Initialize ConductorEngine and run loop
|
||||
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode)
|
||||
# Use current full markdown context for the track execution
|
||||
track_id_param = track.id
|
||||
@@ -1963,23 +1922,22 @@ class AppController:
|
||||
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
|
||||
"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.ui_files_base_dir)
|
||||
|
||||
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 models.Ticket objects)
|
||||
# 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
|
||||
|
||||
existing = project_manager.load_track_state(self.active_track.id, self.ui_files_base_dir)
|
||||
meta = models.Metadata(
|
||||
id=self.active_track.id,
|
||||
|
||||
73
src/gui_2.py
73
src/gui_2.py
@@ -84,14 +84,13 @@ class App:
|
||||
"""The main ImGui interface orchestrator for Manual Slop."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Initialize controller and delegate state
|
||||
# Initialize controller and delegate state
|
||||
self.controller = app_controller.AppController()
|
||||
# Restore legacy PROVIDERS to controller if needed (it already has it via delegation if set on class level, but let's be explicit)
|
||||
if not hasattr(self.controller, 'PROVIDERS'):
|
||||
self.controller.PROVIDERS = PROVIDERS
|
||||
self.controller.init_state()
|
||||
self.controller.start_services(self)
|
||||
|
||||
# Aliases for controller-owned locks
|
||||
self._send_thread_lock = self.controller._send_thread_lock
|
||||
self._disc_entries_lock = self.controller._disc_entries_lock
|
||||
@@ -101,7 +100,6 @@ class App:
|
||||
self._pending_gui_tasks_lock = self.controller._pending_gui_tasks_lock
|
||||
self._pending_dialog_lock = self.controller._pending_dialog_lock
|
||||
self._api_event_queue_lock = self.controller._api_event_queue_lock
|
||||
|
||||
self._discussion_names_cache: list[str] = []
|
||||
self._discussion_names_dirty: bool = True
|
||||
|
||||
@@ -145,10 +143,8 @@ class App:
|
||||
@current_model.setter
|
||||
def current_model(self, value: str) -> None:
|
||||
self.controller.current_model = value
|
||||
|
||||
# ---------------------------------------------------------------- project loading
|
||||
|
||||
# ---------------------------------------------------------------- logic
|
||||
# ---------------------------------------------------------------- project loading
|
||||
# ---------------------------------------------------------------- logic
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Cleanly shuts down the app's background tasks and saves state."""
|
||||
@@ -160,14 +156,7 @@ class App:
|
||||
os.makedirs("tests/artifacts", exist_ok=True)
|
||||
with open("tests/artifacts/temp_callback_output.txt", "w") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- helpers
|
||||
# ---------------------------------------------------------------- helpers
|
||||
|
||||
def _render_text_viewer(self, label: str, content: str) -> None:
|
||||
if imgui.button("[+]##" + str(id(content))):
|
||||
@@ -239,10 +228,10 @@ class App:
|
||||
# Process GUI task queue
|
||||
# DEBUG: Check if tasks exist before processing
|
||||
if hasattr(self, 'controller') and hasattr(self.controller, '_pending_gui_tasks'):
|
||||
pending_count = len(self.controller._pending_gui_tasks)
|
||||
if pending_count > 0:
|
||||
sys.stderr.write(f"[DEBUG gui_2] _gui_func: found {pending_count} pending tasks\n")
|
||||
sys.stderr.flush()
|
||||
pending_count = len(self.controller._pending_gui_tasks)
|
||||
if pending_count > 0:
|
||||
sys.stderr.write(f"[DEBUG gui_2] _gui_func: found {pending_count} pending tasks\n")
|
||||
sys.stderr.flush()
|
||||
self._process_pending_gui_tasks()
|
||||
self._process_pending_history_adds()
|
||||
self._render_track_proposal_modal()
|
||||
@@ -336,7 +325,7 @@ class App:
|
||||
imgui.begin_child("HistoryChild", size=(0, -200))
|
||||
self._render_discussion_panel()
|
||||
imgui.end_child()
|
||||
# Bottom part with tabs for message and response
|
||||
# Bottom part with tabs for message and response
|
||||
if imgui.begin_tab_bar("MessageResponseTabs"):
|
||||
if imgui.begin_tab_item("Message")[0]:
|
||||
self._render_message_panel()
|
||||
@@ -738,10 +727,6 @@ class App:
|
||||
if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)):
|
||||
self._cb_plan_epic()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _render_track_proposal_modal(self) -> None:
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.open_popup("Track Proposal")
|
||||
@@ -756,15 +741,15 @@ class App:
|
||||
imgui.text("No tracks generated.")
|
||||
else:
|
||||
for idx, track in enumerate(self.proposed_tracks):
|
||||
# Title Edit
|
||||
# Title Edit
|
||||
changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', ''))
|
||||
if changed_t:
|
||||
track['title'] = new_t
|
||||
# Goal Edit
|
||||
# Goal Edit
|
||||
changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60))
|
||||
if changed_g:
|
||||
track['goal'] = new_g
|
||||
# Buttons
|
||||
# Buttons
|
||||
if imgui.button(f"Remove##{idx}"):
|
||||
self.proposed_tracks.pop(idx)
|
||||
break
|
||||
@@ -788,7 +773,7 @@ class App:
|
||||
if not exp:
|
||||
imgui.end()
|
||||
return
|
||||
registry = log_registry.LogRegistry("logs/log_registry.toml")
|
||||
registry = log_registry.LogRegistry("logs/sessions/log_registry.toml")
|
||||
sessions = registry.data
|
||||
if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
|
||||
imgui.table_setup_column("Session ID")
|
||||
@@ -1218,7 +1203,6 @@ class App:
|
||||
toks = msg.get("tokens", 0)
|
||||
imgui.text_disabled(f" [{role}] ~{toks:,} tokens")
|
||||
shown += 1
|
||||
|
||||
imgui.separator()
|
||||
if ai_client._provider == "gemini":
|
||||
if ai_client._gemini_cache is not None:
|
||||
@@ -1307,11 +1291,6 @@ class App:
|
||||
if is_blinking:
|
||||
imgui.pop_style_color(2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _render_tool_calls_panel(self) -> None:
|
||||
imgui.text("Tool call history")
|
||||
imgui.same_line()
|
||||
@@ -1349,7 +1328,7 @@ class App:
|
||||
imgui.begin_child(f"tc_script_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar)
|
||||
imgui.input_text_multiline(f"##tc_script_res_{i}", script, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
imgui.end_child()
|
||||
# Result Display
|
||||
# Result Display
|
||||
imgui.text_colored(C_LBL, "Output:")
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[+]##output_{i}"):
|
||||
@@ -1370,7 +1349,7 @@ class App:
|
||||
imgui.end_child()
|
||||
|
||||
def _render_mma_dashboard(self) -> None:
|
||||
# Task 5.3: Dense Summary Line
|
||||
# Task 5.3: Dense Summary Line
|
||||
track_name = self.active_track.description if self.active_track else "None"
|
||||
total_tickets = len(self.active_tickets)
|
||||
done_tickets = sum(1 for t in self.active_tickets if t.get('status') == 'complete')
|
||||
@@ -1380,7 +1359,6 @@ class App:
|
||||
in_t = stats.get('input', 0)
|
||||
out_t = stats.get('output', 0)
|
||||
total_cost += cost_tracker.estimate_cost(model, in_t, out_t)
|
||||
|
||||
imgui.text("Track:")
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_VAL, track_name)
|
||||
@@ -1402,7 +1380,6 @@ class App:
|
||||
elif self.mma_status == "error": status_col = imgui.ImVec4(1, 0, 0, 1)
|
||||
imgui.text_colored(status_col, self.mma_status.upper())
|
||||
imgui.separator()
|
||||
|
||||
# 0. Conductor Setup
|
||||
if imgui.collapsing_header("Conductor Setup"):
|
||||
if imgui.button("Run Setup Scan"):
|
||||
@@ -1410,7 +1387,7 @@ class App:
|
||||
if self.ui_conductor_setup_summary:
|
||||
imgui.input_text_multiline("##setup_summary", self.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only)
|
||||
imgui.separator()
|
||||
# 1. Track Browser
|
||||
# 1. Track Browser
|
||||
imgui.text("Track Browser")
|
||||
if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
|
||||
imgui.table_setup_column("Title")
|
||||
@@ -1449,8 +1426,7 @@ class App:
|
||||
if imgui.button(f"Load##{track.get('id')}"):
|
||||
self._cb_load_track(str(track.get("id") or ""))
|
||||
imgui.end_table()
|
||||
|
||||
# 1b. New Track Form
|
||||
# 1b. New Track Form
|
||||
imgui.text("Create New Track")
|
||||
changed_n, self.ui_new_track_name = imgui.input_text("Name##new_track", self.ui_new_track_name)
|
||||
changed_d, self.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", self.ui_new_track_desc, imgui.ImVec2(-1, 60))
|
||||
@@ -1465,7 +1441,6 @@ class App:
|
||||
self._cb_create_track(self.ui_new_track_name, self.ui_new_track_desc, self.ui_new_track_type)
|
||||
self.ui_new_track_name = ""
|
||||
self.ui_new_track_desc = ""
|
||||
|
||||
imgui.separator()
|
||||
# 2. Global Controls
|
||||
changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode)
|
||||
@@ -1477,7 +1452,7 @@ class App:
|
||||
if self.active_tier:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
||||
# Approval pending indicator
|
||||
# Approval pending indicator
|
||||
any_pending = (
|
||||
self._pending_mma_spawn is not None or
|
||||
self._pending_mma_approval is not None or
|
||||
@@ -1532,8 +1507,7 @@ class App:
|
||||
cost = cost_tracker.estimate_cost(model, in_t, out_t)
|
||||
total_cost += cost
|
||||
imgui.text(f"${cost:,.4f}")
|
||||
|
||||
# Total Row
|
||||
# Total Row
|
||||
imgui.table_next_row()
|
||||
imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered))
|
||||
imgui.table_next_column()
|
||||
@@ -1582,13 +1556,12 @@ class App:
|
||||
rendered: set[str] = set()
|
||||
for root in roots:
|
||||
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
|
||||
|
||||
# 5. Add Ticket Form
|
||||
# 5. Add Ticket Form
|
||||
imgui.separator()
|
||||
if imgui.button("Add Ticket"):
|
||||
self._show_add_ticket_form = not self._show_add_ticket_form
|
||||
if self._show_add_ticket_form:
|
||||
# Default Ticket ID
|
||||
# Default Ticket ID
|
||||
max_id = 0
|
||||
for t in self.active_tickets:
|
||||
tid = t.get('id', '')
|
||||
@@ -1599,7 +1572,6 @@ class App:
|
||||
self.ui_new_ticket_desc = ""
|
||||
self.ui_new_ticket_target = ""
|
||||
self.ui_new_ticket_deps = ""
|
||||
|
||||
if self._show_add_ticket_form:
|
||||
imgui.begin_child("add_ticket_form", imgui.ImVec2(-1, 220), True)
|
||||
imgui.text_colored(C_VAL, "New Ticket Details")
|
||||
@@ -1607,7 +1579,6 @@ class App:
|
||||
_, self.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", self.ui_new_ticket_desc, imgui.ImVec2(-1, 60))
|
||||
_, self.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", self.ui_new_ticket_target)
|
||||
_, self.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", self.ui_new_ticket_deps)
|
||||
|
||||
if imgui.button("Create"):
|
||||
new_ticket = {
|
||||
"id": self.ui_new_ticket_id,
|
||||
@@ -1871,7 +1842,6 @@ class App:
|
||||
def _render_theme_panel(self) -> None:
|
||||
exp, opened = imgui.begin("Theme", self.show_windows["Theme"])
|
||||
self.show_windows["Theme"] = bool(opened)
|
||||
|
||||
if exp:
|
||||
imgui.text("Palette")
|
||||
cp = theme.get_current_palette()
|
||||
@@ -1919,7 +1889,6 @@ class App:
|
||||
|
||||
def run(self) -> None:
|
||||
"""Initializes the ImGui runner and starts the main application loop."""
|
||||
|
||||
if "--headless" in sys.argv:
|
||||
print("Headless mode active")
|
||||
self._fetch_models(self.current_provider)
|
||||
|
||||
Reference in New Issue
Block a user