feat(gui): implement Phases 2-5 of Comprehensive GUI UX track
- Add cost tracking with new cost_tracker.py module - Enhance Track Proposal modal with editable titles and goals - Add Conductor Setup summary and New Track creation form to MMA Dashboard - Implement Task DAG editing (add/delete tickets) and track-scoped discussion - Add visual polish: color-coded statuses, tinted progress bars, and node indicators - Support live worker streaming from AI providers to GUI panels - Fix numerous integration test regressions and stabilize headless service
This commit is contained in:
444
gui_2.py
444
gui_2.py
@@ -15,6 +15,7 @@ from tkinter import filedialog, Tk
|
||||
from typing import Optional, Callable, Any, Dict, List, Tuple, Union
|
||||
import aggregate
|
||||
import ai_client
|
||||
import cost_tracker
|
||||
from ai_client import ProviderError
|
||||
import shell_runner
|
||||
import session_logger
|
||||
@@ -92,7 +93,8 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) -
|
||||
known = roles if roles is not None else DISC_ROLES
|
||||
entries = []
|
||||
for raw in history:
|
||||
entries.append(project_manager.str_to_entry(raw, known))
|
||||
entry = project_manager.str_to_entry(raw, known)
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
class ConfirmDialog:
|
||||
@@ -146,6 +148,15 @@ class MMASpawnApprovalDialog:
|
||||
'context_md': self._context_md
|
||||
}
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
prompt: str
|
||||
auto_add_history: bool = True
|
||||
temperature: float | None = None
|
||||
max_tokens: int | None = None
|
||||
|
||||
class ConfirmRequest(BaseModel):
|
||||
approved: bool
|
||||
|
||||
class App:
|
||||
"""The main ImGui interface orchestrator for Manual Slop."""
|
||||
|
||||
@@ -193,6 +204,10 @@ class App:
|
||||
self.ui_epic_input = ""
|
||||
self.proposed_tracks: list[dict[str, Any]] = []
|
||||
self._show_track_proposal_modal = False
|
||||
self.ui_new_track_name = ""
|
||||
self.ui_new_track_desc = ""
|
||||
self.ui_new_track_type = "feature"
|
||||
self.ui_conductor_setup_summary = ""
|
||||
self.ui_last_script_text = ""
|
||||
self.ui_last_script_output = ""
|
||||
self.ai_status = "idle"
|
||||
@@ -246,14 +261,11 @@ class App:
|
||||
self._mma_spawn_edit_mode = False
|
||||
self._mma_spawn_prompt = ''
|
||||
self._mma_spawn_context = ''
|
||||
self.ui_epic_input = ""
|
||||
self.proposed_tracks: list[dict[str, Any]] = []
|
||||
self._show_track_proposal_modal = False
|
||||
self.mma_tier_usage = {
|
||||
"Tier 1": {"input": 0, "output": 0},
|
||||
"Tier 2": {"input": 0, "output": 0},
|
||||
"Tier 3": {"input": 0, "output": 0},
|
||||
"Tier 4": {"input": 0, "output": 0},
|
||||
"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._tool_log: list[tuple[str, str, float]] = []
|
||||
self._comms_log: list[dict[str, Any]] = []
|
||||
@@ -285,6 +297,16 @@ class App:
|
||||
agent_tools_cfg = self.project.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools: dict[str, bool] = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
self.tracks: list[dict[str, Any]] = []
|
||||
self.ui_conductor_setup_summary = ""
|
||||
self.ui_new_track_name = ""
|
||||
self.ui_new_track_desc = ""
|
||||
self.ui_new_track_type = "feature"
|
||||
self._show_add_ticket_form = False
|
||||
self.ui_new_ticket_id = ""
|
||||
self.ui_new_ticket_desc = ""
|
||||
self.ui_new_ticket_target = ""
|
||||
self.ui_new_ticket_deps = ""
|
||||
self._track_discussion_active = False
|
||||
self.mma_streams: dict[str, str] = {}
|
||||
self._tier_stream_last_len: dict[str, int] = {}
|
||||
self.is_viewing_prior_session = False
|
||||
@@ -379,7 +401,9 @@ class App:
|
||||
'show_confirm_modal': 'show_confirm_modal',
|
||||
'mma_epic_input': 'ui_epic_input',
|
||||
'mma_status': 'mma_status',
|
||||
'mma_active_tier': 'active_tier'
|
||||
'mma_active_tier': 'active_tier',
|
||||
'ui_new_track_name': 'ui_new_track_name',
|
||||
'ui_new_track_desc': 'ui_new_track_desc'
|
||||
}
|
||||
self._clickable_actions: dict[str, Callable[..., Any]] = {
|
||||
'btn_reset': self._handle_reset_session,
|
||||
@@ -392,6 +416,7 @@ class App:
|
||||
'btn_mma_plan_epic': self._cb_plan_epic,
|
||||
'btn_mma_accept_tracks': self._cb_accept_tracks,
|
||||
'btn_mma_start_track': self._cb_start_track,
|
||||
'btn_mma_create_track': lambda: self._cb_create_track(self.ui_new_track_name, self.ui_new_track_desc, self.ui_new_track_type),
|
||||
'btn_approve_tool': self._handle_approve_tool,
|
||||
'btn_approve_script': self._handle_approve_script,
|
||||
'btn_approve_mma_step': self._handle_approve_mma_step,
|
||||
@@ -407,14 +432,6 @@ class App:
|
||||
"""Creates and configures the FastAPI application for headless mode."""
|
||||
api = FastAPI(title="Manual Slop Headless API")
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
prompt: str
|
||||
auto_add_history: bool = True
|
||||
temperature: float | None = None
|
||||
max_tokens: int | None = None
|
||||
|
||||
class ConfirmRequest(BaseModel):
|
||||
approved: bool
|
||||
API_KEY_NAME = "X-API-KEY"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
@@ -752,6 +769,7 @@ class App:
|
||||
self.ai_status = f"discussion not found: {name}"
|
||||
return
|
||||
self.active_discussion = name
|
||||
self._track_discussion_active = False
|
||||
disc_sec["active"] = name
|
||||
self._discussion_names_dirty = True
|
||||
disc_data = discussions[name]
|
||||
@@ -760,7 +778,7 @@ class App:
|
||||
|
||||
def _flush_disc_entries_to_project(self) -> None:
|
||||
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
|
||||
if self.active_track:
|
||||
if self.active_track and self._track_discussion_active:
|
||||
project_manager.save_track_history(self.active_track.id, history_strings, self.ui_files_base_dir)
|
||||
return
|
||||
disc_sec = self.project.setdefault("discussion", {})
|
||||
@@ -879,6 +897,14 @@ class App:
|
||||
"collapsed": False,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
elif action == "mma_stream_append":
|
||||
payload = task.get("payload", {})
|
||||
stream_id = payload.get("stream_id")
|
||||
text = payload.get("text", "")
|
||||
if stream_id:
|
||||
if stream_id not in self.mma_streams:
|
||||
self.mma_streams[stream_id] = ""
|
||||
self.mma_streams[stream_id] += text
|
||||
elif action == "show_track_proposal":
|
||||
self.proposed_tracks = task.get("payload", [])
|
||||
self._show_track_proposal_modal = True
|
||||
@@ -904,8 +930,6 @@ class App:
|
||||
if item in self._settable_fields:
|
||||
attr_name = self._settable_fields[item]
|
||||
setattr(self, attr_name, value)
|
||||
if item == "current_provider" or item == "current_model":
|
||||
ai_client.set_provider(self.current_provider, self.current_model)
|
||||
if item == "gcli_path":
|
||||
if not ai_client._gemini_cli_adapter:
|
||||
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=value)
|
||||
@@ -1188,6 +1212,12 @@ class App:
|
||||
"action": "mma_state_update",
|
||||
"payload": payload
|
||||
})
|
||||
elif event_name == "mma_stream":
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({
|
||||
"action": "mma_stream_append",
|
||||
"payload": payload
|
||||
})
|
||||
elif event_name in ("mma_spawn_approval", "mma_step_approval"):
|
||||
# Route approval events to GUI tasks — payload already has the
|
||||
# correct structure for _process_pending_gui_tasks handlers.
|
||||
@@ -2203,8 +2233,19 @@ class App:
|
||||
imgui.text("No tracks generated.")
|
||||
else:
|
||||
for idx, track in enumerate(self.proposed_tracks):
|
||||
imgui.text_colored(C_LBL, f"Track {idx+1}: {track.get('title', 'Untitled')}")
|
||||
imgui.text_wrapped(f"Goal: {track.get('goal', 'N/A')}")
|
||||
# Title Edit
|
||||
changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', ''))
|
||||
if changed_t:
|
||||
track['title'] = new_t
|
||||
# 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
|
||||
if imgui.button(f"Remove##{idx}"):
|
||||
self.proposed_tracks.pop(idx)
|
||||
break
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Start This Track##{idx}"):
|
||||
self._cb_start_track(idx)
|
||||
imgui.separator()
|
||||
@@ -2391,6 +2432,19 @@ class App:
|
||||
if is_selected:
|
||||
imgui.set_item_default_focus()
|
||||
imgui.end_combo()
|
||||
if self.active_track:
|
||||
imgui.same_line()
|
||||
changed, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active)
|
||||
if changed:
|
||||
if self._track_discussion_active:
|
||||
self._flush_disc_entries_to_project()
|
||||
history_strings = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
|
||||
self.disc_entries = _parse_history_entries(history_strings, self.disc_roles)
|
||||
self.ai_status = f"track discussion: {self.active_track.id}"
|
||||
else:
|
||||
self._flush_disc_entries_to_project()
|
||||
# Restore project discussion
|
||||
self._switch_discussion(self.active_discussion)
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
||||
git_commit = disc_data.get("git_commit", "")
|
||||
@@ -2676,7 +2730,210 @@ class App:
|
||||
self._loop
|
||||
)
|
||||
|
||||
def _cb_run_conductor_setup(self) -> None:
|
||||
base = Path("conductor")
|
||||
if not base.exists():
|
||||
self.ui_conductor_setup_summary = "Error: conductor/ directory not found."
|
||||
return
|
||||
files = list(base.glob("**/*"))
|
||||
files = [f for f in files if f.is_file()]
|
||||
summary = [f"Conductor Directory: {base.absolute()}"]
|
||||
summary.append(f"Total Files: {len(files)}")
|
||||
total_lines = 0
|
||||
for f in files:
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fd:
|
||||
lines = len(fd.readlines())
|
||||
total_lines += lines
|
||||
summary.append(f"- {f.relative_to(base)}: {lines} lines")
|
||||
except Exception:
|
||||
summary.append(f"- {f.relative_to(base)}: Error reading")
|
||||
summary.append(f"Total Line Count: {total_lines}")
|
||||
tracks_dir = base / "tracks"
|
||||
if tracks_dir.exists():
|
||||
tracks = [d for d in tracks_dir.iterdir() if d.is_dir()]
|
||||
summary.append(f"Total Tracks Found: {len(tracks)}")
|
||||
else:
|
||||
summary.append("Tracks Directory: Not found")
|
||||
self.ui_conductor_setup_summary = "\n".join(summary)
|
||||
|
||||
def _cb_create_track(self, name: str, desc: str, track_type: str) -> None:
|
||||
if not name: return
|
||||
track_id = name.lower().replace(" ", "_")
|
||||
track_dir = Path("conductor/tracks") / track_id
|
||||
track_dir.mkdir(parents=True, exist_ok=True)
|
||||
spec_file = track_dir / "spec.md"
|
||||
with open(spec_file, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Specification: {name}\n\nType: {track_type}\n\nDescription: {desc}\n")
|
||||
plan_file = track_dir / "plan.md"
|
||||
with open(plan_file, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Implementation Plan: {name}\n\n- [ ] Task 1: Initialize\n")
|
||||
meta_file = track_dir / "metadata.json"
|
||||
import json
|
||||
with open(meta_file, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"id": track_id,
|
||||
"title": name,
|
||||
"description": desc,
|
||||
"type": track_type,
|
||||
"status": "proposed",
|
||||
"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 Ticket objects)
|
||||
self.active_track.tickets = [Ticket.from_dict(t) for t in self.active_tickets]
|
||||
# Save the state to disk
|
||||
from project_manager import save_track_state, load_track_state
|
||||
from models import TrackState, Metadata
|
||||
from datetime import datetime
|
||||
|
||||
existing = load_track_state(self.active_track.id, self.ui_files_base_dir)
|
||||
meta = Metadata(
|
||||
id=self.active_track.id,
|
||||
name=self.active_track.description,
|
||||
status=self.mma_status,
|
||||
created_at=existing.metadata.created_at if existing else datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
state = TrackState(
|
||||
metadata=meta,
|
||||
discussion=existing.discussion if existing else [],
|
||||
tasks=self.active_track.tickets
|
||||
)
|
||||
save_track_state(self.active_track.id, state, self.ui_files_base_dir)
|
||||
|
||||
def _render_tool_calls_panel(self) -> None:
|
||||
imgui.text("Tool call history")
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear##tc"):
|
||||
self._tool_log.clear()
|
||||
imgui.separator()
|
||||
if imgui.begin_child("tc_scroll"):
|
||||
clipper = imgui.ListClipper()
|
||||
clipper.begin(len(self._tool_log))
|
||||
while clipper.step():
|
||||
for i_minus_one in range(clipper.display_start, clipper.display_end):
|
||||
i = i_minus_one + 1
|
||||
script, result, _ = self._tool_log[i_minus_one]
|
||||
first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)"
|
||||
imgui.text_colored(C_KEY, f"Call #{i}: {first_line}")
|
||||
# Script Display
|
||||
imgui.text_colored(C_LBL, "Script:")
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[+]##script_{i}"):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = f"Call Script #{i}"
|
||||
self.text_viewer_content = script
|
||||
if self.ui_word_wrap:
|
||||
if imgui.begin_child(f"tc_script_wrap_{i}", imgui.ImVec2(-1, 72), True):
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(script)
|
||||
imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
if 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
|
||||
imgui.text_colored(C_LBL, "Output:")
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[+]##output_{i}"):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = f"Call Output #{i}"
|
||||
self.text_viewer_content = result
|
||||
if self.ui_word_wrap:
|
||||
if imgui.begin_child(f"tc_res_wrap_{i}", imgui.ImVec2(-1, 72), True):
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(result)
|
||||
imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
if imgui.begin_child(f"tc_res_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar):
|
||||
imgui.input_text_multiline(f"##tc_res_val_{i}", result, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
imgui.end_child()
|
||||
|
||||
def _render_comms_history_panel(self) -> None:
|
||||
imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}")
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear##comms"):
|
||||
ai_client.clear_comms_log()
|
||||
self._comms_log.clear()
|
||||
imgui.same_line()
|
||||
if imgui.button("Load Log"):
|
||||
self._cb_load_prior_log()
|
||||
if self.is_viewing_prior_session:
|
||||
imgui.same_line()
|
||||
if imgui.button("Exit Prior Session"):
|
||||
self.is_viewing_prior_session = False
|
||||
self.prior_session_entries.clear()
|
||||
self.ai_status = "idle"
|
||||
imgui.separator()
|
||||
imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION")
|
||||
imgui.separator()
|
||||
if imgui.begin_child("comms_scroll"):
|
||||
clipper = imgui.ListClipper()
|
||||
clipper.begin(len(self._comms_log))
|
||||
while clipper.step():
|
||||
for i in range(clipper.display_start, clipper.display_end):
|
||||
entry = self._comms_log[i]
|
||||
imgui.text_colored(C_KEY, f"[{entry.get('direction')}] {entry.get('type')}")
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[+]##c{i}"):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = f"Comms Entry #{i}"
|
||||
self.text_viewer_content = json.dumps(entry.get("payload"), indent=2)
|
||||
imgui.text_unformatted(str(entry.get("payload"))[:200] + "...")
|
||||
imgui.separator()
|
||||
imgui.end_child()
|
||||
|
||||
def _render_mma_dashboard(self) -> None:
|
||||
# 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')
|
||||
total_cost = 0.0
|
||||
for stats in self.mma_tier_usage.values():
|
||||
model = stats.get('model', 'unknown')
|
||||
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)
|
||||
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.same_line()
|
||||
status_col = imgui.ImVec4(1, 1, 1, 1)
|
||||
if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1)
|
||||
elif self.mma_status == "running": status_col = imgui.ImVec4(1, 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)
|
||||
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"):
|
||||
self._cb_run_conductor_setup()
|
||||
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
|
||||
imgui.text("Track Browser")
|
||||
if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable):
|
||||
@@ -2690,14 +2947,49 @@ class App:
|
||||
imgui.table_next_column()
|
||||
imgui.text(track.get("title", "Untitled"))
|
||||
imgui.table_next_column()
|
||||
imgui.text(track.get("status", "unknown"))
|
||||
status = track.get("status", "unknown").lower()
|
||||
if status == "new":
|
||||
imgui.text_colored(imgui.ImVec4(0.7, 0.7, 0.7, 1.0), "NEW")
|
||||
elif status == "active":
|
||||
imgui.text_colored(imgui.ImVec4(1.0, 1.0, 0.0, 1.0), "ACTIVE")
|
||||
elif status == "done":
|
||||
imgui.text_colored(imgui.ImVec4(0.0, 1.0, 0.0, 1.0), "DONE")
|
||||
elif status == "blocked":
|
||||
imgui.text_colored(imgui.ImVec4(1.0, 0.0, 0.0, 1.0), "BLOCKED")
|
||||
else:
|
||||
imgui.text(status)
|
||||
imgui.table_next_column()
|
||||
progress = track.get("progress", 0.0)
|
||||
if progress < 0.33:
|
||||
p_color = imgui.ImVec4(1.0, 0.0, 0.0, 1.0)
|
||||
elif progress < 0.66:
|
||||
p_color = imgui.ImVec4(1.0, 1.0, 0.0, 1.0)
|
||||
else:
|
||||
p_color = imgui.ImVec4(0.0, 1.0, 0.0, 1.0)
|
||||
imgui.push_style_color(imgui.Col_.plot_histogram, p_color)
|
||||
imgui.progress_bar(progress, imgui.ImVec2(-1, 0), f"{int(progress*100)}%")
|
||||
imgui.pop_style_color()
|
||||
imgui.table_next_column()
|
||||
if imgui.button(f"Load##{track.get('id')}"):
|
||||
self._cb_load_track(track.get("id"))
|
||||
imgui.end_table()
|
||||
|
||||
# 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))
|
||||
imgui.text("Type:")
|
||||
imgui.same_line()
|
||||
if imgui.begin_combo("##track_type", self.ui_new_track_type):
|
||||
for ttype in ["feature", "chore", "fix"]:
|
||||
if imgui.selectable(ttype, self.ui_new_track_type == ttype)[0]:
|
||||
self.ui_new_track_type = ttype
|
||||
imgui.end_combo()
|
||||
if imgui.button("Create Track"):
|
||||
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)
|
||||
@@ -2737,21 +3029,47 @@ class App:
|
||||
imgui.text_disabled("No active MMA track.")
|
||||
# 3. Token Usage Table
|
||||
imgui.separator()
|
||||
imgui.text("Tier Usage (Tokens)")
|
||||
if imgui.begin_table("mma_usage", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
|
||||
imgui.text("Tier Usage (Tokens & Cost)")
|
||||
if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg):
|
||||
imgui.table_setup_column("Tier")
|
||||
imgui.table_setup_column("Model")
|
||||
imgui.table_setup_column("Input")
|
||||
imgui.table_setup_column("Output")
|
||||
imgui.table_setup_column("Est. Cost")
|
||||
imgui.table_headers_row()
|
||||
usage = self.mma_tier_usage
|
||||
total_cost = 0.0
|
||||
for tier, stats in usage.items():
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text(tier)
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{stats.get('input', 0):,}")
|
||||
model = stats.get('model', 'unknown')
|
||||
imgui.text(model)
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"{stats.get('output', 0):,}")
|
||||
in_t = stats.get('input', 0)
|
||||
imgui.text(f"{in_t:,}")
|
||||
imgui.table_next_column()
|
||||
out_t = stats.get('output', 0)
|
||||
imgui.text(f"{out_t:,}")
|
||||
imgui.table_next_column()
|
||||
cost = cost_tracker.estimate_cost(model, in_t, out_t)
|
||||
total_cost += cost
|
||||
imgui.text(f"${cost:,.4f}")
|
||||
|
||||
# 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()
|
||||
imgui.text("TOTAL")
|
||||
imgui.table_next_column()
|
||||
imgui.text("")
|
||||
imgui.table_next_column()
|
||||
imgui.text("")
|
||||
imgui.table_next_column()
|
||||
imgui.text("")
|
||||
imgui.table_next_column()
|
||||
imgui.text(f"${total_cost:,.4f}")
|
||||
imgui.end_table()
|
||||
imgui.separator()
|
||||
# 4. Task DAG Visualizer
|
||||
@@ -2775,6 +3093,48 @@ class App:
|
||||
rendered = set()
|
||||
for root in roots:
|
||||
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
|
||||
|
||||
# 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
|
||||
max_id = 0
|
||||
for t in self.active_tickets:
|
||||
tid = t.get('id', '')
|
||||
if tid.startswith('T-'):
|
||||
try: max_id = max(max_id, int(tid[2:]))
|
||||
except: pass
|
||||
self.ui_new_ticket_id = f"T-{max_id + 1:03d}"
|
||||
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")
|
||||
_, self.ui_new_ticket_id = imgui.input_text("ID##new_ticket", self.ui_new_ticket_id)
|
||||
_, 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,
|
||||
"description": self.ui_new_ticket_desc,
|
||||
"status": "todo",
|
||||
"assigned_to": "tier3-worker",
|
||||
"target_file": self.ui_new_ticket_target,
|
||||
"depends_on": [d.strip() for d in self.ui_new_ticket_deps.split(",") if d.strip()]
|
||||
}
|
||||
self.active_tickets.append(new_ticket)
|
||||
self._show_add_ticket_form = False
|
||||
self._push_mma_state_update()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel"):
|
||||
self._show_add_ticket_form = False
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.text_disabled("No active MMA track.")
|
||||
|
||||
@@ -2812,24 +3172,25 @@ class App:
|
||||
tid = ticket.get('id', '??')
|
||||
target = ticket.get('target_file', 'general')
|
||||
status = ticket.get('status', 'pending').upper()
|
||||
# Determine color
|
||||
status_color = vec4(200, 200, 200) # Gray (TODO)
|
||||
status_color = vec4(178, 178, 178)
|
||||
if status == 'RUNNING':
|
||||
status_color = vec4(255, 255, 0) # Yellow
|
||||
status_color = vec4(255, 255, 0)
|
||||
elif status == 'COMPLETE':
|
||||
status_color = vec4(0, 255, 0) # Green
|
||||
status_color = vec4(0, 255, 0)
|
||||
elif status in ['BLOCKED', 'ERROR']:
|
||||
status_color = vec4(255, 0, 0) # Red
|
||||
status_color = vec4(255, 0, 0)
|
||||
elif status == 'PAUSED':
|
||||
status_color = vec4(255, 165, 0) # Orange
|
||||
status_color = vec4(255, 165, 0)
|
||||
p_min = imgui.get_cursor_screen_pos()
|
||||
p_max = imgui.ImVec2(p_min.x + 4, p_min.y + imgui.get_text_line_height_with_spacing())
|
||||
imgui.get_window_draw_list().add_rect_filled(p_min, p_max, imgui.get_color_u32(status_color))
|
||||
imgui.set_cursor_screen_pos(imgui.ImVec2(p_min.x + 8, p_min.y))
|
||||
flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open
|
||||
children = children_map.get(tid, [])
|
||||
if not children:
|
||||
flags |= imgui.TreeNodeFlags_.leaf
|
||||
# Check if already rendered elsewhere to avoid infinite recursion or duplicate subtrees
|
||||
is_duplicate = tid in rendered
|
||||
node_open = imgui.tree_node_ex(f"##{tid}", flags)
|
||||
# Detail View / Tooltip
|
||||
if imgui.is_item_hovered():
|
||||
imgui.begin_tooltip()
|
||||
imgui.text_colored(C_KEY, f"ID: {tid}")
|
||||
@@ -2858,6 +3219,15 @@ class App:
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Skip##{tid}"):
|
||||
self._cb_ticket_skip(tid)
|
||||
if status in ['TODO', 'BLOCKED']:
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Delete##{tid}"):
|
||||
self.active_tickets = [t for t in self.active_tickets if t.get('id') != tid]
|
||||
for t in self.active_tickets:
|
||||
deps = t.get('depends_on', [])
|
||||
if tid in deps:
|
||||
t['depends_on'] = [d for d in deps if d != tid]
|
||||
self._push_mma_state_update()
|
||||
if node_open:
|
||||
if not is_duplicate:
|
||||
rendered.add(tid)
|
||||
@@ -2868,10 +3238,6 @@ class App:
|
||||
else:
|
||||
imgui.text_disabled(" (shown above)")
|
||||
imgui.tree_pop()
|
||||
|
||||
def _render_tool_calls_panel(self) -> None:
|
||||
imgui.text("Tool call history")
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear##tc"):
|
||||
self._tool_log.clear()
|
||||
imgui.separator()
|
||||
|
||||
Reference in New Issue
Block a user