diff --git a/gui_2.py b/gui_2.py index eef3995..746afc2 100644 --- a/gui_2.py +++ b/gui_2.py @@ -131,6 +131,29 @@ class MMAApprovalDialog: return self._approved, self._payload +class MMASpawnApprovalDialog: + def __init__(self, ticket_id: str, role: str, prompt: str, context_md: str): + self._ticket_id = ticket_id + self._role = role + self._prompt = prompt + self._context_md = context_md + self._condition = threading.Condition() + self._done = False + self._approved = False + self._abort = False + + def wait(self) -> dict: + with self._condition: + while not self._done: + self._condition.wait(timeout=0.1) + return { + 'approved': self._approved, + 'abort': self._abort, + 'prompt': self._prompt, + 'context_md': self._context_md + } + + class App: """The main ImGui interface orchestrator for Manual Slop.""" @@ -246,6 +269,13 @@ class App: self._mma_approval_edit_mode = False self._mma_approval_payload = "" + # MMA Spawn approval state + self._pending_mma_spawn = None + self._mma_spawn_open = False + self._mma_spawn_edit_mode = False + self._mma_spawn_prompt = '' + self._mma_spawn_context = '' + # Orchestration State self.ui_epic_input = "" self.proposed_tracks: list[dict] = [] @@ -989,6 +1019,21 @@ class App: if "dialog_container" in task: task["dialog_container"][0] = dlg + elif action == "mma_spawn_approval": + dlg = MMASpawnApprovalDialog( + task.get("ticket_id"), + task.get("role"), + task.get("prompt"), + task.get("context_md") + ) + self._pending_mma_spawn = task + self._mma_spawn_prompt = task.get("prompt", "") + self._mma_spawn_context = task.get("context_md", "") + self._mma_spawn_open = True + self._mma_spawn_edit_mode = False + if "dialog_container" in task: + task["dialog_container"][0] = dlg + except Exception as e: print(f"Error executing GUI task: {e}") @@ -1020,7 +1065,7 @@ class App: else: print("[DEBUG] No pending dialog to reject") - def _handle_mma_respond(self, approved: bool, payload: str = None): + def _handle_mma_respond(self, approved: bool, payload: str = None, abort: bool = False, prompt: str = None, context_md: str = None): if self._pending_mma_approval: dlg = self._pending_mma_approval.get("dialog_container", [None])[0] if dlg: @@ -1031,6 +1076,20 @@ class App: dlg._done = True dlg._condition.notify_all() self._pending_mma_approval = None + + if self._pending_mma_spawn: + dlg = self._pending_mma_spawn.get("dialog_container", [None])[0] + if dlg: + with dlg._condition: + dlg._approved = approved + dlg._abort = abort + if prompt is not None: + dlg._prompt = prompt + if context_md is not None: + dlg._context_md = context_md + dlg._done = True + dlg._condition.notify_all() + self._pending_mma_spawn = None def _handle_approve_ask(self): """Responds with approval for a pending /api/ask request.""" @@ -1821,6 +1880,54 @@ class App: imgui.close_current_popup() imgui.end_popup() + # MMA Spawn Approval Modal + if self._pending_mma_spawn: + if not self._mma_spawn_open: + imgui.open_popup("MMA Spawn Approval") + self._mma_spawn_open = True + self._mma_spawn_edit_mode = False + else: + self._mma_spawn_open = False + + if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not self._pending_mma_spawn: + imgui.close_current_popup() + else: + role = self._pending_mma_spawn.get("role", "??") + ticket_id = self._pending_mma_spawn.get("ticket_id", "??") + imgui.text(f"Spawning {role} for Ticket {ticket_id}") + imgui.separator() + + if self._mma_spawn_edit_mode: + imgui.text("Edit Prompt:") + _, self._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", self._mma_spawn_prompt, imgui.ImVec2(800, 200)) + imgui.text("Edit Context MD:") + _, self._mma_spawn_context = imgui.input_text_multiline("##spawn_context", self._mma_spawn_context, imgui.ImVec2(800, 300)) + else: + imgui.text("Proposed Prompt:") + imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True) + imgui.text_unformatted(self._mma_spawn_prompt) + imgui.end_child() + imgui.text("Proposed Context MD:") + imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True) + imgui.text_unformatted(self._mma_spawn_context) + imgui.end_child() + + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): + self._handle_mma_respond(approved=True, prompt=self._mma_spawn_prompt, context_md=self._mma_spawn_context) + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Edit Mode" if not self._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): + self._mma_spawn_edit_mode = not self._mma_spawn_edit_mode + + imgui.same_line() + if imgui.button("Abort", imgui.ImVec2(120, 0)): + self._handle_mma_respond(approved=False, abort=True) + imgui.close_current_popup() + imgui.end_popup() + if self.show_script_output: if self._trigger_script_blink: self._trigger_script_blink = False diff --git a/multi_agent_conductor.py b/multi_agent_conductor.py index e767cc9..5cb186a 100644 --- a/multi_agent_conductor.py +++ b/multi_agent_conductor.py @@ -217,22 +217,27 @@ def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.A start = time.time() while dialog_container[0] is None and time.time() - start < 60: time.sleep(0.1) - - if dialog_container[0]: - approved, final_payload = dialog_container[0].wait() - - # Extract modifications from final_payload if it's a dict - modified_prompt = prompt - modified_context = context_md - - if isinstance(final_payload, dict): - modified_prompt = final_payload.get("prompt", prompt) - modified_context = final_payload.get("context_md", context_md) - - return approved, modified_prompt, modified_context - - return False, prompt, context_md + if dialog_container[0]: + res = dialog_container[0].wait() + + if isinstance(res, dict): + approved = res.get("approved", False) + abort = res.get("abort", False) + modified_prompt = res.get("prompt", prompt) + modified_context = res.get("context_md", context_md) + return approved and not abort, modified_prompt, modified_context + else: + # Fallback for old tuple style if any + approved, final_payload = res + modified_prompt = prompt + modified_context = context_md + if isinstance(final_payload, dict): + modified_prompt = final_payload.get("prompt", prompt) + modified_context = final_payload.get("context_md", context_md) + return approved, modified_prompt, modified_context + + return False, prompt, context_md def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] = None, event_queue: events.AsyncEventQueue = None, engine: Optional['ConductorEngine'] = None, md_content: str = ""): """ Simulates the lifecycle of a single agent working on a ticket. diff --git a/tests/test_spawn_interception.py b/tests/test_spawn_interception.py index d8ed3c5..1424c3d 100644 --- a/tests/test_spawn_interception.py +++ b/tests/test_spawn_interception.py @@ -11,7 +11,11 @@ class MockDialog: self.approved = approved self.final_payload = final_payload def wait(self): - return self.approved, self.final_payload + # Match the new return format: a dictionary + res = {'approved': self.approved, 'abort': False} + if self.final_payload: + res.update(self.final_payload) + return res @pytest.fixture def mock_ai_client():