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, "Context Hub": True,
"Files & Media": True, "Files & Media": True,
"AI Settings": True, "AI Settings": True,
"MMA Dashboard": True,
"Discussion Hub": True, "Discussion Hub": True,
"Operations Hub": True, "Operations Hub": True,
"Theme": True, "Theme": True,
@@ -209,6 +210,19 @@ class App:
self._ask_request_id = None self._ask_request_id = None
self._ask_tool_data = 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._tool_log: list[tuple[str, str]] = []
self._comms_log: list[dict] = [] self._comms_log: list[dict] = []
@@ -827,6 +841,13 @@ class App:
"ts": project_manager.now_ts() "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": elif action == "set_value":
item = task.get("item") item = task.get("item")
value = task.get("value") value = task.get("value")
@@ -1014,6 +1035,12 @@ class App:
"action": "handle_ai_response", "action": "handle_ai_response",
"payload": payload "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): def _handle_request_event(self, event: events.UserRequestEvent):
"""Processes a UserRequestEvent by calling the AI client.""" """Processes a UserRequestEvent by calling the AI client."""
@@ -1430,6 +1457,12 @@ class App:
self._render_system_prompts_panel() self._render_system_prompts_panel()
imgui.end() 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): if self.show_windows.get("Theme", False):
self._render_theme_panel() self._render_theme_panel()
@@ -1606,6 +1639,48 @@ class App:
imgui.close_current_popup() imgui.close_current_popup()
imgui.end_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.show_script_output:
if self._trigger_script_blink: if self._trigger_script_blink:
self._trigger_script_blink = False self._trigger_script_blink = False
@@ -2257,6 +2332,72 @@ class App:
if is_blinking: if is_blinking:
imgui.pop_style_color(2) 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): def _render_tool_calls_panel(self):
imgui.text("Tool call history") imgui.text("Tool call history")
imgui.same_line() imgui.same_line()

View File

@@ -1,6 +1,9 @@
import ai_client import ai_client
import json import json
import asyncio
from typing import List, Optional from typing import List, Optional
from dataclasses import asdict
import events
from models import Ticket, Track, WorkerContext from models import Ticket, Track, WorkerContext
from file_cache import ASTParser from file_cache import ASTParser
@@ -8,8 +11,24 @@ class ConductorEngine:
""" """
Orchestrates the execution of tickets within a track. 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.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): def parse_json_tickets(self, json_str: str):
""" """
@@ -38,13 +57,15 @@ class ConductorEngine:
except KeyError as e: except KeyError as e:
print(f"Missing required field in ticket definition: {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. Executes tickets sequentially according to their dependencies.
Iterates through the track's executable tickets until no more can be run. 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 Supports dynamic execution as tickets added during runtime will be picked up
in the next iteration of the main loop. in the next iteration of the main loop.
""" """
await self._push_state(status="running", active_tier="Tier 2 (Tech Lead)")
while True: while True:
executable = self.track.get_executable_tickets() executable = self.track.get_executable_tickets()
if not executable: if not executable:
@@ -52,14 +73,17 @@ class ConductorEngine:
all_done = all(t.status == "completed" for t in self.track.tickets) all_done = all(t.status == "completed" for t in self.track.tickets)
if all_done: if all_done:
print("Track completed successfully.") print("Track completed successfully.")
await self._push_state(status="done", active_tier=None)
else: else:
# If we have no executable tickets but some are not completed, we might be blocked # 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. # or there are simply no more tickets to run at this moment.
incomplete = [t for t in self.track.tickets if t.status != "completed"] incomplete = [t for t in self.track.tickets if t.status != "completed"]
if not incomplete: if not incomplete:
print("Track completed successfully.") print("Track completed successfully.")
await self._push_state(status="done", active_tier=None)
else: else:
print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.") print(f"No more executable tickets. {len(incomplete)} tickets remain incomplete.")
await self._push_state(status="blocked", active_tier=None)
break break
for ticket in executable: for ticket in executable:
@@ -69,6 +93,9 @@ class ConductorEngine:
continue continue
print(f"Executing ticket {ticket.id}: {ticket.description}") 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 # For now, we use a default model name or take it from config
context = WorkerContext( context = WorkerContext(
ticket_id=ticket.id, ticket_id=ticket.id,
@@ -76,6 +103,7 @@ class ConductorEngine:
messages=[] messages=[]
) )
run_worker_lifecycle(ticket, context) run_worker_lifecycle(ticket, context)
await self._push_state(active_tier="Tier 2 (Tech Lead)")
def confirm_execution(payload: str) -> bool: def confirm_execution(payload: str) -> bool:
""" """