feat(ui): Implement enhanced MMA track progress visualization with color-coded bars, breakdown, and ETA

This commit is contained in:
2026-03-07 11:42:35 -05:00
parent 87902d82d8
commit 1e188fd3aa
4 changed files with 170 additions and 31 deletions

View File

@@ -149,6 +149,9 @@ class AppController:
self._cached_cache_stats: Dict[str, Any] = {} # Pre-computed cache stats for GUI self._cached_cache_stats: Dict[str, Any] = {} # Pre-computed cache stats for GUI
self._token_history: List[Dict[str, Any]] = [] # Token usage over time [{"time": t, "input": n, "output": n, "model": s}, ...] self._token_history: List[Dict[str, Any]] = [] # Token usage over time [{"time": t, "input": n, "output": n, "model": s}, ...]
self._session_start_time: float = time.time() # For calculating burn rate self._session_start_time: float = time.time() # For calculating burn rate
self._ticket_start_times: dict[str, float] = {}
self._avg_ticket_time: float = 0.0
self._completed_ticket_count: int = 0
self._comms_log: List[Dict[str, Any]] = [] self._comms_log: List[Dict[str, Any]] = []
self.session_usage: Dict[str, Any] = { self.session_usage: Dict[str, Any] = {
"input_tokens": 0, "input_tokens": 0,
@@ -573,6 +576,21 @@ class AppController:
self._mma_spawn_edit_mode = False self._mma_spawn_edit_mode = False
if "dialog_container" in task: if "dialog_container" in task:
task["dialog_container"][0] = spawn_dlg task["dialog_container"][0] = spawn_dlg
elif action == "ticket_started":
payload = task.get("payload", {})
ticket_id = payload.get("ticket_id")
start_time = payload.get("timestamp")
if ticket_id and start_time:
self._ticket_start_times[ticket_id] = start_time
elif action == "ticket_completed":
payload = task.get("payload", {})
ticket_id = payload.get("ticket_id")
end_time = payload.get("timestamp")
if ticket_id and end_time and ticket_id in self._ticket_start_times:
start_time = self._ticket_start_times.pop(ticket_id)
elapsed = end_time - start_time
self._completed_ticket_count += 1
self._avg_ticket_time = ((self._avg_ticket_time * (self._completed_ticket_count - 1)) + elapsed) / self._completed_ticket_count
except Exception as e: except Exception as e:
import traceback import traceback
sys.stderr.write(f"[DEBUG] Error executing GUI task: {e}\n{traceback.format_exc()}\n") sys.stderr.write(f"[DEBUG] Error executing GUI task: {e}\n{traceback.format_exc()}\n")
@@ -902,6 +920,18 @@ class AppController:
if self.test_hooks_enabled: if self.test_hooks_enabled:
with self._api_event_queue_lock: with self._api_event_queue_lock:
self._api_event_queue.append({"type": "response", "payload": payload}) self._api_event_queue.append({"type": "response", "payload": payload})
elif event_name == "ticket_started":
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({
"action": "ticket_started",
"payload": payload
})
elif event_name == "ticket_completed":
with self._pending_gui_tasks_lock:
self._pending_gui_tasks.append({
"action": "ticket_completed",
"payload": payload
})
def _handle_request_event(self, event: events.UserRequestEvent) -> None: def _handle_request_event(self, event: events.UserRequestEvent) -> None:
"""Processes a UserRequestEvent by calling the AI client.""" """Processes a UserRequestEvent by calling the AI client."""

View File

@@ -1,4 +1,4 @@
# gui_2.py # gui_2.py
from __future__ import annotations from __future__ import annotations
import tomli_w import tomli_w
import time import time
@@ -1752,26 +1752,21 @@ class App:
def _render_mma_dashboard(self) -> None: 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" track_name = self.active_track.description if self.active_track else "None"
total_tickets = len(self.active_tickets) track_stats = {"percentage": 0.0, "completed": 0, "total": 0, "in_progress": 0, "blocked": 0, "todo": 0}
done_tickets = sum(1 for t in self.active_tickets if t.get('status') == 'complete') if self.active_track:
track_stats = project_manager.calculate_track_progress(self.active_track.tickets)
total_cost = 0.0 total_cost = 0.0
for stats in self.mma_tier_usage.values(): for usage in self.mma_tier_usage.values():
model = stats.get('model', 'unknown') model = usage.get('model', 'unknown')
in_t = stats.get('input', 0) in_t = usage.get('input', 0)
out_t = stats.get('output', 0) out_t = usage.get('output', 0)
total_cost += cost_tracker.estimate_cost(model, in_t, out_t) total_cost += cost_tracker.estimate_cost(model, in_t, out_t)
imgui.text("Track:") imgui.text("Track:")
imgui.same_line() imgui.same_line()
imgui.text_colored(C_VAL, track_name) imgui.text_colored(C_VAL, track_name)
imgui.same_line() imgui.same_line()
imgui.text(" | Tickets:")
imgui.same_line()
imgui.text_colored(C_VAL, f"{done_tickets}/{total_tickets}")
imgui.same_line()
imgui.text(" | Cost:")
imgui.same_line()
imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}")
imgui.same_line()
imgui.text(" | Status:") imgui.text(" | Status:")
imgui.same_line() imgui.same_line()
status_col = imgui.ImVec4(1, 1, 1, 1) status_col = imgui.ImVec4(1, 1, 1, 1)
@@ -1780,7 +1775,54 @@ class App:
elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1)
elif self.mma_status == "error": status_col = imgui.ImVec4(1, 0, 0, 1) elif self.mma_status == "error": status_col = imgui.ImVec4(1, 0, 0, 1)
imgui.text_colored(status_col, self.mma_status.upper()) imgui.text_colored(status_col, self.mma_status.upper())
imgui.same_line()
imgui.text(" | Cost:")
imgui.same_line()
imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}")
# Progress Bar
perc = track_stats["percentage"] / 100.0
p_color = imgui.ImVec4(0.0, 1.0, 0.0, 1.0)
if track_stats["percentage"] < 33:
p_color = imgui.ImVec4(1.0, 0.0, 0.0, 1.0)
elif track_stats["percentage"] < 66:
p_color = imgui.ImVec4(1.0, 1.0, 0.0, 1.0)
imgui.push_style_color(imgui.Col_.plot_histogram, p_color)
imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%")
imgui.pop_style_color()
# Detailed breakdown
if imgui.begin_table("ticket_stats_breakdown", 4):
imgui.table_next_column()
imgui.text_colored(C_LBL, "Completed:")
imgui.same_line()
imgui.text_colored(C_VAL, str(track_stats["completed"]))
imgui.table_next_column()
imgui.text_colored(C_LBL, "In Progress:")
imgui.same_line()
imgui.text_colored(C_VAL, str(track_stats["in_progress"]))
imgui.table_next_column()
imgui.text_colored(C_LBL, "Blocked:")
imgui.same_line()
imgui.text_colored(C_VAL, str(track_stats["blocked"]))
imgui.table_next_column()
imgui.text_colored(C_LBL, "Todo:")
imgui.same_line()
imgui.text_colored(C_VAL, str(track_stats["todo"]))
imgui.end_table()
if self.active_track:
remaining = track_stats["total"] - track_stats["completed"]
eta_mins = (self._avg_ticket_time * remaining) / 60.0
imgui.text_colored(C_LBL, "ETA:")
imgui.same_line()
imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)")
imgui.separator() imgui.separator()
imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)')
_, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80)) _, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80))
@@ -1874,20 +1916,7 @@ class App:
if imgui.button("Go to Approval"): if imgui.button("Go to Approval"):
pass # scroll/focus handled by existing dialog rendering pass # scroll/focus handled by existing dialog rendering
imgui.separator() imgui.separator()
# 2. Active Track Info # 3. Token Usage Table
if self.active_track:
imgui.text(f"Track: {self.active_track.description}")
# 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.")
# 3. Token Usage Table
imgui.separator()
imgui.text("Tier Usage (Tokens & Cost)") imgui.text("Tier Usage (Tokens & Cost)")
if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
imgui.table_setup_column("Tier") imgui.table_setup_column("Tier")

View File

@@ -218,6 +218,7 @@ class ConductorEngine:
if spawned: if spawned:
ticket.status = "in_progress" ticket.status = "in_progress"
_queue_put(self.event_queue, "ticket_started", {"ticket_id": ticket.id, "timestamp": time.time()})
print(f"Executing ticket {ticket.id}: {ticket.description}") print(f"Executing ticket {ticket.id}: {ticket.description}")
self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}") self._push_state(active_tier=f"Tier 3 (Worker): {ticket.id}")
@@ -368,6 +369,8 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
) )
if not approved: if not approved:
ticket.mark_blocked("Spawn rejected by user.") ticket.mark_blocked("Spawn rejected by user.")
if event_queue:
_queue_put(event_queue, "ticket_completed", {"ticket_id": ticket.id, "timestamp": time.time()})
return "BLOCKED: Spawn rejected by user." return "BLOCKED: Spawn rejected by user."
user_message = modified_prompt user_message = modified_prompt
md_content = modified_context md_content = modified_context
@@ -417,7 +420,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
ai_client.comms_log_callback = old_comms_cb ai_client.comms_log_callback = old_comms_cb
ai_client.set_current_tier(None) ai_client.set_current_tier(None)
if event_queue: if event_queue:
# Push via "response" event type _process_event_queue wraps this # Push via "response" event type — _process_event_queue wraps this
# as {"action": "handle_ai_response", "payload": ...} for the GUI. # as {"action": "handle_ai_response", "payload": ...} for the GUI.
try: try:
response_payload = { response_payload = {
@@ -441,4 +444,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
ticket.mark_blocked(response) ticket.mark_blocked(response)
else: else:
ticket.mark_complete() ticket.mark_complete()
if event_queue:
_queue_put(event_queue, "ticket_completed", {"ticket_id": ticket.id, "timestamp": time.time()})
return response return response

View File

@@ -0,0 +1,74 @@
import pytest
from unittest.mock import MagicMock, patch
from src.gui_2 import App, C_LBL, C_VAL
from src.models import Ticket
def test_render_mma_dashboard_progress():
# Create a mock for the imgui module used in gui_2
mock_imgui = MagicMock()
# Configure ImVec4 and ImVec2
mock_imgui.ImVec2 = MagicMock(side_effect=lambda x, y: (float(x), float(y)))
mock_imgui.ImVec4 = MagicMock(side_effect=lambda r, g, b, a: (float(r), float(g), float(b), float(a)))
# Configure calls that return tuples
mock_imgui.input_text_multiline.return_value = (False, "")
mock_imgui.input_text.return_value = (False, "")
mock_imgui.checkbox.return_value = (False, False)
mock_imgui.begin_combo.return_value = (False, "")
mock_imgui.selectable.return_value = (False, False)
mock_imgui.begin_table.return_value = True
mock_imgui.collapsing_header.return_value = False
# Patch where it is actually used
with patch('src.gui_2.imgui', mock_imgui), \
patch('src.gui_2.cost_tracker.estimate_cost', return_value=0.0):
# Mock App instance - no spec because of delegation
app = MagicMock()
# Setup mock state
app.active_track = MagicMock()
app.active_track.description = "Test Track"
# Mock self.active_track.tickets as a list of src.models.Ticket objects
app.active_track.tickets = [
Ticket(id='T1', description='desc', status='completed'),
Ticket(id='T2', description='desc', status='in_progress'),
Ticket(id='T3', description='desc', status='blocked'),
Ticket(id='T4', description='desc', status='todo')
]
app.mma_tier_usage = {}
app.mma_status = "idle"
app.active_tier = None
app._pending_mma_spawn = None
app._pending_mma_approval = None
app._pending_ask_dialog = False
app.tracks = []
app.ui_epic_input = ""
app.ui_conductor_setup_summary = ""
app.ui_new_track_name = ""
app.ui_new_track_desc = ""
app.ui_new_track_type = "feature"
app.mma_step_mode = False
app.node_editor_ctx = None
app._avg_ticket_time = 60
# Call the method
App._render_mma_dashboard(app)
# Assertions
# 1 completed out of 4 tickets = 25.0% progress
# Update assertions: imgui.progress_bar is called with (0.25, (-1.0, 0.0), '25.0%')
mock_imgui.progress_bar.assert_any_call(0.25, (-1.0, 0.0), "25.0%")
# Verify status breakdown counts are asserted via imgui.text_colored calls
mock_imgui.text_colored.assert_any_call(C_LBL, "Completed:")
mock_imgui.text_colored.assert_any_call(C_VAL, "1")
mock_imgui.text_colored.assert_any_call(C_LBL, "In Progress:")
mock_imgui.text_colored.assert_any_call(C_VAL, "1")
mock_imgui.text_colored.assert_any_call(C_LBL, "Blocked:")
mock_imgui.text_colored.assert_any_call(C_VAL, "1")
mock_imgui.text_colored.assert_any_call(C_LBL, "Todo:")
mock_imgui.text_colored.assert_any_call(C_VAL, "1")