pain
This commit is contained in:
+322
-143
@@ -1021,6 +1021,84 @@ class App:
|
||||
|
||||
# Modals / Popups
|
||||
self._render_approve_script_modal()
|
||||
self._render_mma_modals()
|
||||
|
||||
def _render_mma_modals(self) -> None:
|
||||
"""Renders all MMA-specific approval and info modals."""
|
||||
is_nerv = theme.is_nerv_active()
|
||||
# Tool Execution Approval
|
||||
if self._pending_ask_dialog:
|
||||
if not self._ask_dialog_open:
|
||||
imgui.open_popup("Approve Tool Execution")
|
||||
self._ask_dialog_open = True
|
||||
else:
|
||||
self._ask_dialog_open = False
|
||||
if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
if not self._pending_ask_dialog or self._ask_tool_data is None: imgui.close_current_popup()
|
||||
else:
|
||||
tool_name = self._ask_tool_data.get("tool", "unknown"); tool_args = self._ask_tool_data.get("args", {})
|
||||
imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator()
|
||||
imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child()
|
||||
imgui.separator()
|
||||
if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_approve_ask(); imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Deny", imgui.ImVec2(120, 0)): self._handle_reject_ask(); imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
# MMA Step Approval
|
||||
if self._pending_mma_approvals:
|
||||
if not self._mma_approval_open:
|
||||
imgui.open_popup("MMA Step Approval")
|
||||
self._mma_approval_open, self._mma_approval_edit_mode = True, False
|
||||
self._mma_approval_payload = self._pending_mma_approvals[0].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_approvals: imgui.close_current_popup()
|
||||
else:
|
||||
ticket_id = self._pending_mma_approvals[0].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(str(self._pending_mma_approvals[0].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()
|
||||
# MMA Spawn Approval
|
||||
if self._pending_mma_spawns:
|
||||
if not self._mma_spawn_open:
|
||||
imgui.open_popup("MMA Spawn Approval")
|
||||
self._mma_spawn_open, self._mma_spawn_edit_mode = True, False
|
||||
self._mma_spawn_prompt, self._mma_spawn_context = self._pending_mma_spawns[0].get("prompt", ""), self._pending_mma_spawns[0].get("context_md", "")
|
||||
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_spawns: imgui.close_current_popup()
|
||||
else:
|
||||
role, ticket_id = self._pending_mma_spawns[0].get("role", "??"), self._pending_mma_spawns[0].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()
|
||||
# Cycle Detection
|
||||
if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!")
|
||||
imgui.text("Please remove the circular dependency.")
|
||||
if imgui.button("OK"):
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_approve_script_modal(self) -> None:
|
||||
"""Renders the modal dialog for approving AI-generated PowerShell scripts."""
|
||||
@@ -1547,41 +1625,6 @@ class App:
|
||||
with imscope.tab_item("Beads") as (exp, _):
|
||||
if exp: self._render_beads_tab()
|
||||
imgui.end_tab_bar()
|
||||
|
||||
if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
if not dlg:
|
||||
imgui.close_current_popup()
|
||||
else:
|
||||
imgui.text("The AI wants to run the following PowerShell script:")
|
||||
imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}")
|
||||
imgui.separator()
|
||||
# Checkbox to toggle full preview inside modal
|
||||
_, self.show_text_viewer = imgui.checkbox("Show Full Preview", self.show_text_viewer)
|
||||
if self.show_text_viewer:
|
||||
imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True)
|
||||
imgui.text_unformatted(dlg._script)
|
||||
imgui.end_child()
|
||||
else:
|
||||
ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200))
|
||||
imgui.separator()
|
||||
if imgui.button("Approve & Run", imgui.ImVec2(120, 0)):
|
||||
with dlg._condition:
|
||||
dlg._approved = True
|
||||
dlg._done = True
|
||||
dlg._condition.notify_all()
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_dialog = None
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Reject", imgui.ImVec2(120, 0)):
|
||||
with dlg._condition:
|
||||
dlg._approved = False
|
||||
dlg._done = True
|
||||
dlg._condition.notify_all()
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_dialog = None
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
if self._pending_ask_dialog:
|
||||
if not self._ask_dialog_open:
|
||||
@@ -1644,112 +1687,6 @@ class App:
|
||||
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()
|
||||
#endregion: MMA Step Approval Modal
|
||||
|
||||
#region: MMA Spawn Approval Modal
|
||||
if self._pending_mma_spawns:
|
||||
if not self._mma_spawn_open:
|
||||
imgui.open_popup("MMA Spawn Approval")
|
||||
self._mma_spawn_open = True
|
||||
self._mma_spawn_edit_mode = False
|
||||
self._mma_spawn_prompt = self._pending_mma_spawns[0].get("prompt", "")
|
||||
self._mma_spawn_context = self._pending_mma_spawns[0].get("context_md", "")
|
||||
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_spawns:
|
||||
imgui.close_current_popup()
|
||||
else:
|
||||
role = self._pending_mma_spawns[0].get("role", "??")
|
||||
ticket_id = self._pending_mma_spawns[0].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()
|
||||
#endregion: MMA Spawn Approval Modal
|
||||
|
||||
#region: Cycle Detected Popup
|
||||
if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!")
|
||||
imgui.text("Please remove the circular dependency.")
|
||||
if imgui.button("OK"):
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
if self.show_script_output:
|
||||
if self._trigger_script_blink:
|
||||
self._trigger_script_blink = False
|
||||
self._is_script_blinking = True
|
||||
self._script_blink_start_time = time.time()
|
||||
try:
|
||||
imgui.set_window_focus("Last Script Output") # type: ignore[call-arg]
|
||||
except Exception:
|
||||
pass
|
||||
if self._is_script_blinking:
|
||||
elapsed = time.time() - self._script_blink_start_time
|
||||
if elapsed > 1.5:
|
||||
self._is_script_blinking = False
|
||||
else:
|
||||
val = math.sin(elapsed * 8 * math.pi)
|
||||
alpha = 60/255 if val > 0 else 0
|
||||
imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 100, 255, alpha))
|
||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(0, 100, 255, alpha))
|
||||
imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever)
|
||||
expanded, opened = imgui.begin("Last Script Output", self.show_script_output)
|
||||
self.show_script_output = bool(opened)
|
||||
if expanded:
|
||||
imgui.text("Script:")
|
||||
imgui.same_line()
|
||||
self._render_text_viewer("Last Script", self.ui_last_script_text)
|
||||
if self.ui_word_wrap:
|
||||
imgui.begin_child("lso_s_wrap", imgui.ImVec2(-1, 200), True)
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(self.ui_last_script_text)
|
||||
imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.input_text_multiline("##lso_s", self.ui_last_script_text, imgui.ImVec2(-1, 200), imgui.InputTextFlags_.read_only)
|
||||
imgui.separator()
|
||||
imgui.text("Output:")
|
||||
imgui.same_line()
|
||||
self._render_text_viewer("Last Output", self.ui_last_script_output)
|
||||
if self.ui_word_wrap:
|
||||
imgui.begin_child("lso_o_wrap", imgui.ImVec2(-1, -1), True)
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(self.ui_last_script_output)
|
||||
imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.input_text_multiline("##lso_o", self.ui_last_script_output, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
|
||||
if self._is_script_blinking:
|
||||
imgui.pop_style_color(2)
|
||||
imgui.end()
|
||||
@@ -4820,8 +4757,7 @@ def hello():
|
||||
self._render_mma_agent_streams()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard")
|
||||
|
||||
def _render_task_dag_panel(self) -> None:
|
||||
# 4. Task DAG Visualizer
|
||||
def _render_task_dag_panel(self) -> None: # 4. Task DAG Visualizer
|
||||
imgui.text("Task DAG")
|
||||
if (self.active_track or self.active_tickets) and self.node_editor_ctx:
|
||||
ed.set_current_editor(self.node_editor_ctx)
|
||||
@@ -5371,6 +5307,249 @@ def hello():
|
||||
imgui.end()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel")
|
||||
|
||||
def _render_thinking_indicator(self) -> None:
|
||||
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
||||
if is_thinking:
|
||||
val = math.sin(time.time() * 10 * math.pi)
|
||||
alpha = 1.0 if val > 0 else 0.0
|
||||
c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha)
|
||||
imgui.text_colored(c, "THINKING..."); imgui.same_line()
|
||||
|
||||
def _render_prior_session_view(self) -> None:
|
||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
||||
if imgui.button("Exit Prior Session"): self.controller.cb_exit_prior_session(); self._comms_log_dirty = True
|
||||
imgui.separator()
|
||||
with imscope.child("prior_scroll"):
|
||||
clipper = imgui.ListClipper(); clipper.begin(len(self.prior_disc_entries))
|
||||
while clipper.step():
|
||||
for idx in range(clipper.display_start, clipper.display_end):
|
||||
entry = self.prior_disc_entries[idx];
|
||||
with imscope.id(f"prior_disc_{idx}"):
|
||||
collapsed = entry.get("collapsed", False)
|
||||
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
|
||||
imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "")
|
||||
imgui.text_colored(C_LBL, f"[{role}]")
|
||||
if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts))
|
||||
content = entry.get("content", "")
|
||||
if collapsed:
|
||||
imgui.same_line(); preview = content.replace("\n", " ")[:80]
|
||||
if len(content) > 80: preview += "..."
|
||||
imgui.text_colored(vec4(180, 180, 180), preview)
|
||||
else:
|
||||
is_nerv = theme.is_nerv_active()
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
imgui.separator()
|
||||
imgui.pop_style_color()
|
||||
|
||||
def _render_discussion_selector(self) -> None:
|
||||
if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return
|
||||
names = self._get_discussion_names(); grouped = {}
|
||||
for name in names:
|
||||
base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name)
|
||||
active_base = self.active_discussion.split("_take_")[0]
|
||||
if active_base not in grouped: active_base = names[0] if names else ""
|
||||
base_names = sorted(grouped.keys())
|
||||
if imgui.begin_combo("##disc_sel", active_base):
|
||||
for bname in base_names:
|
||||
is_selected = (bname == active_base)
|
||||
if imgui.selectable(bname, is_selected)[0]:
|
||||
target = bname if bname in names else grouped[bname][0]
|
||||
if target != self.active_discussion: self._switch_discussion(target)
|
||||
if is_selected: imgui.set_item_default_focus()
|
||||
imgui.end_combo()
|
||||
active_base = self.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, [])
|
||||
if imgui.begin_tab_bar("discussion_takes_tabs"):
|
||||
for take_name in current_takes:
|
||||
label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title()
|
||||
flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0
|
||||
with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _):
|
||||
if exp and take_name != self.active_discussion: self._switch_discussion(take_name)
|
||||
with imscope.tab_item("Synthesis###Synthesis") as (exp, _):
|
||||
if exp: self._render_synthesis_panel()
|
||||
imgui.end_tab_bar()
|
||||
if "_take_" in self.active_discussion:
|
||||
if imgui.button("Promote Take"):
|
||||
base_name = self.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1
|
||||
while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1
|
||||
project_manager.promote_take(self.project, self.active_discussion, new_name); self._switch_discussion(new_name)
|
||||
imgui.same_line()
|
||||
if self.active_track:
|
||||
imgui.same_line(); ch, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active)
|
||||
if ch:
|
||||
if self._track_discussion_active:
|
||||
self._flush_disc_entries_to_project()
|
||||
history_strings = project_manager.load_track_history(self.active_track.id, self.active_project_root)
|
||||
with self._disc_entries_lock: self.disc_entries = models.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(); self._switch_discussion(self.active_discussion); self.ai_status = "track discussion disabled"
|
||||
self._render_discussion_metadata()
|
||||
|
||||
def _render_discussion_metadata(self) -> None:
|
||||
disc_data = self.project.get("discussion", {}).get("discussions", {}).get(self.active_discussion, {})
|
||||
git_commit, last_updated = disc_data.get("git_commit", ""), disc_data.get("last_updated", "")
|
||||
imgui.text_colored(C_LBL, "commit:"); imgui.same_line()
|
||||
self._render_selectable_label('git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
||||
imgui.same_line()
|
||||
if imgui.button("Update Commit"):
|
||||
if self.ui_project_git_dir:
|
||||
cmt = project_manager.get_git_commit(self.ui_project_git_dir)
|
||||
if cmt: disc_data["git_commit"], disc_data["last_updated"], self.ai_status = cmt, project_manager.now_ts(), f"commit: {cmt[:12]}"
|
||||
imgui.text_colored(C_LBL, "updated:"); imgui.same_line(); imgui.text_colored(C_SUB, last_updated if last_updated else "(never)")
|
||||
ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input); imgui.same_line()
|
||||
if imgui.button("Create"):
|
||||
nm = self.ui_disc_new_name_input.strip()
|
||||
if nm: self._create_discussion(nm); self.ui_disc_new_name_input = ""
|
||||
imgui.same_line()
|
||||
if imgui.button("Rename"):
|
||||
nm = self.ui_disc_new_name_input.strip()
|
||||
if nm: self._rename_discussion(self.active_discussion, nm); self.ui_disc_new_name_input = ""
|
||||
imgui.same_line()
|
||||
if imgui.button("Delete"): self._delete_discussion(self.active_discussion)
|
||||
|
||||
def _render_discussion_entry_controls(self) -> None:
|
||||
if imgui.button("+ Entry"): self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
if imgui.button("-All"):
|
||||
for e in self.disc_entries: e["collapsed"] = True
|
||||
imgui.same_line()
|
||||
if imgui.button("+All"):
|
||||
for e in self.disc_entries: e["collapsed"] = False
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear All"): self.disc_entries.clear()
|
||||
imgui.same_line()
|
||||
if imgui.button("Save"): self._flush_to_project(); self._flush_to_config(); models.save_config(self.config); self.ai_status = "discussion saved"
|
||||
_, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
||||
imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80)
|
||||
ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1)
|
||||
if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1
|
||||
imgui.same_line()
|
||||
if imgui.button("Truncate"):
|
||||
with self._disc_entries_lock: self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
|
||||
self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs"
|
||||
|
||||
def _render_discussion_roles(self) -> None:
|
||||
if imgui.collapsing_header("Roles"):
|
||||
with imscope.child("roles_scroll", size_y=100, flags=True):
|
||||
for i, r in enumerate(list(self.disc_roles)):
|
||||
with imscope.id(f"role_{i}"):
|
||||
if imgui.button("X"): self.disc_roles.pop(i); break
|
||||
imgui.same_line(); imgui.text(r)
|
||||
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input); imgui.same_line()
|
||||
if imgui.button("Add"):
|
||||
r = self.ui_disc_new_role_input.strip()
|
||||
if r and r not in self.disc_roles: self.disc_roles.append(r); self.ui_disc_new_role_input = ""
|
||||
|
||||
def _render_discussion_entries(self) -> None:
|
||||
with imscope.child("disc_scroll"):
|
||||
display_entries = self.disc_entries
|
||||
if self.ui_focus_agent:
|
||||
tier_usage = self.mma_tier_usage.get(self.ui_focus_agent)
|
||||
if tier_usage:
|
||||
persona_name = tier_usage.get("persona")
|
||||
if persona_name: display_entries = [e for e in self.disc_entries if e.get("role") == persona_name or e.get("role") == "User"]
|
||||
clipper = imgui.ListClipper(); clipper.begin(len(display_entries))
|
||||
while clipper.step():
|
||||
for i in range(clipper.display_start, clipper.display_end):
|
||||
self._render_discussion_entry(display_entries[i], i)
|
||||
if self._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); self._scroll_disc_to_bottom = False
|
||||
|
||||
def _render_discussion_entry(self, entry: dict, index: int) -> None:
|
||||
with imscope.id(f"disc_{index}"):
|
||||
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
|
||||
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
|
||||
imgui.same_line(); self._render_text_viewer(f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120)
|
||||
if imgui.begin_combo("##role", entry["role"]):
|
||||
for r in self.disc_roles:
|
||||
if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r
|
||||
imgui.end_combo()
|
||||
if not collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode
|
||||
ts_str = entry.get("ts", "")
|
||||
if ts_str:
|
||||
imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str)
|
||||
if e_dt:
|
||||
e_unix, next_unix = e_dt.timestamp(), float('inf')
|
||||
if index + 1 < len(self.disc_entries):
|
||||
n_ts = self.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts)
|
||||
if n_dt: next_unix = n_dt.timestamp()
|
||||
injected = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix]
|
||||
if injected:
|
||||
imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]")
|
||||
if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected]))
|
||||
if collapsed:
|
||||
imgui.same_line()
|
||||
if imgui.button("Ins"): self.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()})
|
||||
imgui.same_line()
|
||||
if imgui.button("Del"): self.disc_entries.pop(index); return
|
||||
imgui.same_line()
|
||||
if imgui.button("Branch"): self._branch_discussion(index)
|
||||
imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60]
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
if not preview.strip() and entry.get("thinking_segments"):
|
||||
preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60]
|
||||
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||
if not collapsed:
|
||||
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
|
||||
if thinking_segments: self._render_thinking_trace(thinking_segments, index, is_standalone=not has_content)
|
||||
if read_mode: self._render_discussion_entry_read_mode(entry, index)
|
||||
else:
|
||||
if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
imgui.separator()
|
||||
|
||||
def _render_discussion_entry_read_mode(self, entry: dict, index: int) -> None:
|
||||
content = entry["content"]
|
||||
if not content.strip(): return
|
||||
if '## Retrieved Context' in content:
|
||||
rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content)
|
||||
if rag_match:
|
||||
rag_section = rag_match.group(1)
|
||||
if imgui.collapsing_header('Retrieved Context'):
|
||||
chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section)
|
||||
for chunk_match in chunks:
|
||||
idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3)
|
||||
if imgui.collapsing_header(f'Chunk {idx}: {path}'):
|
||||
if imgui.button(f'[Source]##rag_{index}_{idx}'):
|
||||
res = mcp_client.read_file(path)
|
||||
if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True
|
||||
imgui.text_unformatted(chunk_content)
|
||||
content = content[:rag_match.start()] + content[rag_match.end():]
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active()
|
||||
if not matches:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(content, context_id=f'disc_{index}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
else:
|
||||
with imscope.child(f"read_content_{index}", size_y=150, flags=True):
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
last_idx = 0
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4)
|
||||
if imgui.collapsing_header(header_text):
|
||||
if imgui.button(f"[Source]##{index}_{match.start()}"):
|
||||
res = mcp_client.read_file(path)
|
||||
if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True
|
||||
if code_block:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(after, context_id=f'disc_{index}_a')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
|
||||
def _load_fonts(self) -> None:
|
||||
# Set hello_imgui assets folder to the actual absolute path
|
||||
assets_dir = Path(__file__).parent.parent / "assets"
|
||||
|
||||
+8
-5
@@ -18,13 +18,16 @@ class _ScopeWindow:
|
||||
|
||||
def child(id_str: str, size_x: float = 0, size_y: float = 0, flags: int = 0): return _ScopeChild(id_str, size_x, size_y, flags)
|
||||
class _ScopeChild:
|
||||
def __init__(self, id_str: str, size_x: float, size_y: float, flags: int):
|
||||
def __init__(self, id_str: str, size_x: float | imgui.ImVec2, size_y: float, flags: int):
|
||||
self._id = id_str
|
||||
self._sx = size_x
|
||||
self._sy = size_y
|
||||
# Check if size_x is likely an ImVec2 without using isinstance (which breaks with mocks)
|
||||
if hasattr(size_x, 'x') and hasattr(size_x, 'y'):
|
||||
self._size = size_x
|
||||
else:
|
||||
self._size = imgui.ImVec2(float(size_x), float(size_y))
|
||||
self._flags = flags
|
||||
def __enter__(self):
|
||||
res = imgui.begin_child(self._id, self._sx, self._sy, self._flags)
|
||||
res = imgui.begin_child(self._id, self._size, self._flags)
|
||||
return res
|
||||
def __exit__(self, *args):
|
||||
imgui.end_child()
|
||||
@@ -151,7 +154,7 @@ class _ScopeTabItem:
|
||||
self._expanded = False
|
||||
self._open = None
|
||||
def __enter__(self):
|
||||
self._expanded, self._open = imgui.begin_tab_item(self._label, None, self._flags)
|
||||
self._expanded, self._open = imgui.begin_tab_item(self._label, flags=self._flags)
|
||||
return self._expanded, self._open
|
||||
def __exit__(self, *args):
|
||||
if self._expanded:
|
||||
|
||||
@@ -24,6 +24,7 @@ def mock_gui():
|
||||
|
||||
def test_discussion_tabs_rendered(mock_gui):
|
||||
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||
patch('src.imgui_scopes.imgui', new=mock_imgui), \
|
||||
patch('src.app_controller.AppController.active_project_root', new_callable=PropertyMock, return_value='.'):
|
||||
|
||||
# We expect a combo box for base discussion
|
||||
|
||||
@@ -18,10 +18,11 @@ def test_render_mma_dashboard_progress():
|
||||
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
|
||||
|
||||
mock_imgui.begin_tab_item.return_value = (True, True)
|
||||
mock_imgui.collapsing_header.return_value = False
|
||||
# Patch where it is actually used
|
||||
with patch('src.gui_2.imgui', mock_imgui), \
|
||||
patch('src.imgui_scopes.imgui', new=mock_imgui), \
|
||||
patch('src.gui_2.cost_tracker.estimate_cost', return_value=0.0):
|
||||
|
||||
# Mock App instance - no spec because of delegation
|
||||
@@ -58,8 +59,17 @@ def test_render_mma_dashboard_progress():
|
||||
app._avg_ticket_time = 60
|
||||
|
||||
# Call the method
|
||||
App._render_mma_dashboard(app)
|
||||
|
||||
# Dashboard now delegates to sub-methods, so we wire them up to execute their real logic on the mock instance.
|
||||
app._render_mma_focus_selector.side_effect = lambda: App._render_mma_focus_selector(app)
|
||||
app._render_mma_track_summary.side_effect = lambda: App._render_mma_track_summary(app)
|
||||
app._render_mma_epic_planner.side_effect = lambda: App._render_mma_epic_planner(app)
|
||||
app._render_mma_conductor_setup.side_effect = lambda: App._render_mma_conductor_setup(app)
|
||||
app._render_mma_track_browser.side_effect = lambda: App._render_mma_track_browser(app)
|
||||
app._render_mma_global_controls.side_effect = lambda: App._render_mma_global_controls(app)
|
||||
app._render_mma_usage_section.side_effect = lambda: App._render_mma_usage_section(app)
|
||||
app._render_mma_agent_streams.side_effect = lambda: App._render_mma_agent_streams(app)
|
||||
|
||||
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%')
|
||||
|
||||
Reference in New Issue
Block a user