feat(mma): Implement MMA Dashboard, Event Handling, and Step Approval Modal in gui_2.py

This commit is contained in:
2026-02-26 21:46:05 -05:00
parent 51918d9bc3
commit 63a82e0d15
2 changed files with 171 additions and 2 deletions

141
gui_2.py
View File

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

View File

@@ -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:
"""