diff --git a/gui_2.py b/gui_2.py index f943d59..1716ec5 100644 --- a/gui_2.py +++ b/gui_2.py @@ -185,6 +185,7 @@ class App: "Context Hub": True, "Files & Media": True, "AI Settings": True, + "MMA Dashboard": True, "Discussion Hub": True, "Operations Hub": True, "Theme": True, @@ -209,6 +210,19 @@ class App: self._ask_request_id = None self._ask_tool_data = None + # MMA State + self.mma_step_mode = False + self.active_track = None + self.active_tickets = [] + self.active_tier = None # "Tier 1", "Tier 2", etc. + self.mma_status = "idle" + + # MMA-specific approval state + self._pending_mma_approval = None + self._mma_approval_open = False + self._mma_approval_edit_mode = False + self._mma_approval_payload = "" + self._tool_log: list[tuple[str, str]] = [] self._comms_log: list[dict] = [] @@ -827,6 +841,13 @@ class App: "ts": project_manager.now_ts() }) + elif action == "mma_state_update": + payload = task.get("payload", {}) + self.mma_status = payload.get("status", "idle") + self.active_tier = payload.get("active_tier") + self.active_track = payload.get("track") + self.active_tickets = payload.get("tickets", []) + elif action == "set_value": item = task.get("item") value = task.get("value") @@ -1014,6 +1035,12 @@ class App: "action": "handle_ai_response", "payload": payload }) + elif event_name == "mma_state_update": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "mma_state_update", + "payload": payload + }) def _handle_request_event(self, event: events.UserRequestEvent): """Processes a UserRequestEvent by calling the AI client.""" @@ -1429,6 +1456,12 @@ class App: if imgui.collapsing_header("System Prompts"): self._render_system_prompts_panel() imgui.end() + + if self.show_windows.get("MMA Dashboard", False): + exp, self.show_windows["MMA Dashboard"] = imgui.begin("MMA Dashboard", self.show_windows["MMA Dashboard"]) + if exp: + self._render_mma_dashboard() + imgui.end() if self.show_windows.get("Theme", False): self._render_theme_panel() @@ -1606,6 +1639,48 @@ class App: imgui.close_current_popup() imgui.end_popup() + # MMA Step Approval Modal + if self._pending_mma_approval: + if not self._mma_approval_open: + imgui.open_popup("MMA Step Approval") + self._mma_approval_open = True + self._mma_approval_edit_mode = False + self._mma_approval_payload = self._pending_mma_approval.get("payload", "") + else: + self._mma_approval_open = False + + if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not self._pending_mma_approval: + imgui.close_current_popup() + else: + ticket_id = self._pending_mma_approval.get("ticket_id", "??") + imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval.") + imgui.separator() + + if self._mma_approval_edit_mode: + imgui.text("Edit Raw Payload (Manual Memory Mutation):") + _, self._mma_approval_payload = imgui.input_text_multiline("##mma_payload", self._mma_approval_payload, imgui.ImVec2(600, 400)) + else: + imgui.text("Proposed Tool Call:") + imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(self._pending_mma_approval.get("payload", "")) + imgui.end_child() + + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): + self._handle_mma_respond(approved=True, payload=self._mma_approval_payload) + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Edit Payload" if not self._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): + self._mma_approval_edit_mode = not self._mma_approval_edit_mode + + imgui.same_line() + if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): + self._handle_mma_respond(approved=False) + imgui.close_current_popup() + imgui.end_popup() + if self.show_script_output: if self._trigger_script_blink: self._trigger_script_blink = False @@ -2257,6 +2332,72 @@ class App: if is_blinking: imgui.pop_style_color(2) + def _render_mma_dashboard(self): + # 1. Global Controls + changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode) + if changed: + # We could push an event here if the engine needs to know immediately + pass + + imgui.same_line() + imgui.text(f"Status: {self.mma_status.upper()}") + if self.active_tier: + imgui.same_line() + imgui.text_colored(C_VAL, f"| Active: {self.active_tier}") + + imgui.separator() + + # 2. Active Track Info + if self.active_track: + imgui.text(f"Track: {self.active_track.get('title', 'Unknown')}") + + # Progress bar + tickets = self.active_tickets + total = len(tickets) + if total > 0: + complete = sum(1 for t in tickets if t.get('status') == 'complete') + progress = complete / total + imgui.progress_bar(progress, imgui.ImVec2(-1, 0), f"{complete}/{total} Tickets") + else: + imgui.text_disabled("No active MMA track.") + + imgui.separator() + + # 3. Ticket Queue + imgui.text("Ticket Queue") + if imgui.begin_table("mma_tickets", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable): + imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("Target", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100) + imgui.table_headers_row() + + for t in self.active_tickets: + imgui.table_next_row() + imgui.table_next_column() + imgui.text(str(t.get('id', '??'))) + + imgui.table_next_column() + imgui.text(str(t.get('target_file', 'general'))) + + imgui.table_next_column() + status = t.get('status', 'pending').upper() + + if status == 'RUNNING': + imgui.push_style_color(imgui.Col_.text, vec4(255, 255, 0)) # Yellow + elif status == 'COMPLETE': + imgui.push_style_color(imgui.Col_.text, vec4(0, 255, 0)) # Green + elif status == 'BLOCKED' or status == 'ERROR': + imgui.push_style_color(imgui.Col_.text, vec4(255, 0, 0)) # Red + elif status == 'PAUSED': + imgui.push_style_color(imgui.Col_.text, vec4(255, 165, 0)) # Orange + + imgui.text(status) + + if status in ['RUNNING', 'COMPLETE', 'BLOCKED', 'ERROR', 'PAUSED']: + imgui.pop_style_color() + + imgui.end_table() + def _render_tool_calls_panel(self): imgui.text("Tool call history") imgui.same_line() diff --git a/multi_agent_conductor.py b/multi_agent_conductor.py index 37395c8..b97c171 100644 --- a/multi_agent_conductor.py +++ b/multi_agent_conductor.py @@ -1,6 +1,9 @@ import ai_client import json +import asyncio from typing import List, Optional +from dataclasses import asdict +import events from models import Ticket, Track, WorkerContext from file_cache import ASTParser @@ -8,8 +11,24 @@ class ConductorEngine: """ Orchestrates the execution of tickets within a track. """ - def __init__(self, track: Track): + def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None): self.track = track + self.event_queue = event_queue + + async def _push_state(self, status: str = "running", active_tier: str = None): + if not self.event_queue: + return + + payload = { + "status": status, + "active_tier": active_tier, + "track": { + "id": self.track.id, + "title": self.track.description, + }, + "tickets": [asdict(t) for t in self.track.tickets] + } + await self.event_queue.put("mma_state_update", payload) def parse_json_tickets(self, json_str: str): """ @@ -38,13 +57,15 @@ class ConductorEngine: except KeyError as e: print(f"Missing required field in ticket definition: {e}") - def run_linear(self): + async def run_linear(self): """ Executes tickets sequentially according to their dependencies. Iterates through the track's executable tickets until no more can be run. Supports dynamic execution as tickets added during runtime will be picked up in the next iteration of the main loop. """ + await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)") + while True: executable = self.track.get_executable_tickets() if not executable: @@ -52,14 +73,17 @@ class ConductorEngine: all_done = all(t.status == "completed" for t in self.track.tickets) if all_done: print("Track completed successfully.") + await self._push_state(status="done", active_tier=None) else: # If we have no executable tickets but some are not completed, we might be blocked # or there are simply no more tickets to run at this moment. incomplete = [t for t in self.track.tickets if t.status != "completed"] if not incomplete: print("Track completed successfully.") + await self._push_state(status="done", active_tier=None) else: print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.") + await self._push_state(status="blocked", active_tier=None) break for ticket in executable: @@ -69,6 +93,9 @@ class ConductorEngine: continue print(f"Executing ticket {ticket.id}: {ticket.description}") + ticket.status = "running" + await self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}") + # For now, we use a default model name or take it from config context = WorkerContext( ticket_id=ticket.id, @@ -76,6 +103,7 @@ class ConductorEngine: messages=[] ) run_worker_lifecycle(ticket, context) + await self._push_state(active_tier="Tier 2 (Tech Lead)") def confirm_execution(payload: str) -> bool: """