feat(ux): complete multi-panel case orchestration, presets, and main panel cognitive status

This commit is contained in:
operator
2026-10-10 12:32:45 -04:00
parent 340cc9e85b
commit f1252423a8
2 changed files with 465 additions and 28 deletions
+310 -28
View File
@@ -2641,12 +2641,52 @@ class MuseTUI:
else:
self._btn_menu_bounds = None
# Agent Work & Cognitive Status Sub-header (visible without clicks)
cog = (self.data.cognitive_cache or {}).get(active_node) or {}
c_st = cog.get("status") or "IDLE"
c_locked = cog.get("cognitive_lock", False)
screen = cog.get("screen") or {}
active_scr_title = screen.get("title") or screen.get("url") or ""
assigned_ticket = None
for iss in (getattr(self.data, "work_issues_cache", []) or []):
if not isinstance(iss, dict):
continue
if (iss.get("assignee") or {}).get("username") == active_node and iss.get("state") == "open":
assigned_ticket = f"#{iss.get('number')} {(iss.get('title') or '')[:28]}"
break
div_rule_char = "━" if is_focused else "─"
div_rule_attr = self._attr("user") if is_focused else self._attr("dim")
self.safe_addstr(self.stdscr, y + 1, x, div_rule_char * w, div_rule_attr)
transcript_y = y + 2
transcript_h = h - 2
has_sub_bar = (h >= 14 and w >= 36)
if has_sub_bar:
# Cognitive indicator & active task
cog_str = f"● @{active_node.upper()} [{c_st}]"
cog_attr = self._attr("warn_banner") if c_st == "THINKING" else (self._attr("danger") if c_locked else self._attr("success"))
task_str = f"| Work: {assigned_ticket}" if assigned_ticket else (f"| Context: {active_scr_title[:24]}" if active_scr_title else f"| State: {c_st}")
actions_str = "[w:Case] [f:Refocus] [t:Thoughts] [p:Menu] [h:Heal]"
self.safe_addstr(self.stdscr, y + 1, x, " " * w, self._attr("dim"))
self.safe_addstr(self.stdscr, y + 1, x + 1, cog_str, cog_attr)
pos_x = x + 1 + len(cog_str) + 1
self.safe_addstr(self.stdscr, y + 1, pos_x, task_str[:max(0, w - pos_x - len(actions_str) - 2)], self._attr("dim"))
act_x = max(pos_x + len(task_str) + 2, x + w - len(actions_str) - 1)
if act_x + len(actions_str) <= x + w:
self.safe_addstr(self.stdscr, y + 1, act_x, actions_str, self._attr("wo_badge"))
self._btn_case_actions_bounds = (y + 1, act_x, act_x + len(actions_str))
else:
self._btn_case_actions_bounds = None
self.safe_addstr(self.stdscr, y + 2, x, div_rule_char * w, div_rule_attr)
transcript_y = y + 3
transcript_h = h - 3
else:
self._btn_case_actions_bounds = None
self.safe_addstr(self.stdscr, y + 1, x, div_rule_char * w, div_rule_attr)
transcript_y = y + 2
transcript_h = h - 2
if not msgs:
is_loading_hist = self.data.is_loading_history(active_node, active_tid)
@@ -3072,7 +3112,7 @@ class MuseTUI:
# Footer hints
hint_y = y + h - 1
hints = "[W]: Dispatch Work Ticket [F]: Refocus Main Chat [T]: Thoughts [P]: Menu [L]: Loopback Sweep [H]: Heal [r]: Refresh"
hints = "[W]: Dispatch [a]: Assign Ticket [m]: Merge PR [F]: Refocus [T]: Thoughts [H]: Heal [L]: Loopback [r]: Refresh"
self.safe_addstr(self.stdscr, hint_y, x + 1, hints[:w - 2], self._attr("header"))
def _render_approvals_view(self, y: int, x: int, h: int, w: int):
@@ -3660,7 +3700,7 @@ class MuseTUI:
is_context_popup = self.modal in ("chat_context", "agent_context", "message_context")
if is_context_popup:
modal_w = min(56, screen_w - 4)
modal_h = min(17 if self.modal == "chat_context" else (14 if self.modal == "agent_context" else 11), screen_h - 2)
modal_h = min(17 if self.modal == "chat_context" else (19 if self.modal == "agent_context" else 11), screen_h - 2)
anchor_x, anchor_y = getattr(self, "context_popup_pos", (screen_w // 4, screen_h // 3))
top_y = max(1, min(anchor_y, screen_h - modal_h - 2))
left_x = max(1, min(anchor_x, screen_w - modal_w - 2))
@@ -4133,11 +4173,17 @@ class MuseTUI:
actions = [
("open", "[1] ▶ Open Chat with Agent", "[Enter/o]"),
("approvals", "[2] ⚡ Resolve Approvals", "[a]"),
("tmux", "[3] 📋 Copy Tmux Command", "[t]"),
("wo", "[4] 📝 Issue Work Order", "[w]"),
("sync", "[5] 🔄 Sync Threads", "[s]"),
("read_all", "[6] ✔ Mark All Read", "[m]"),
("dispatch", "[2] 🚀 Dispatch New Case / Build Ticket", "[w]"),
("thoughts", "[3] 🧠 Inspect Live Cognition & Screen", "[t]"),
("menu", "[4] 👤 Navigate Agent Profile Menu", "[p]"),
("refocus", "[5] 🎯 Refocus Agent to Main Chat", "[f]"),
("approvals", "[6] ⚡ Resolve Approvals", "[a]"),
("heal", "[7] 🩺 Heal & Self-Repair Worker", "[h]"),
("loopback", "[8] ⚡ Sweep Loopback Escalation", "[l]"),
("tmux", "[9] 📋 Copy Tmux Command", "[c]"),
("wo", "[0] 📝 Issue Work Order", "[W]"),
("sync", "[s] 🔄 Sync Threads", "[s]"),
("read_all", "[m] ✔ Mark All Read", "[m]"),
]
self._agent_context_actions = actions
self.agent_context_sel = max(0, min(getattr(self, "agent_context_sel", 0), len(actions) - 1))
@@ -4154,7 +4200,7 @@ class MuseTUI:
self.safe_addstr(self.stdscr, cur_y, left_x + 3, f"{pointer}{label}", row_attr)
self.safe_addstr(self.stdscr, cur_y, left_x + modal_w - 3 - len(hotkey), hotkey, self._attr("dim") if not is_sel else self._attr("selected"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[Esc/q] Close [j/k/↑/↓] Navigate [1-8/Click] Run Action", self._attr("dim"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[Esc/q] Close [j/k/↑/↓] Navigate [1-9,0,Hotkeys] Run Action", self._attr("dim"))
elif self.modal == "message_context":
m = getattr(self, "context_message", None) or {}
@@ -4194,8 +4240,16 @@ class MuseTUI:
elif self.modal == "work_dispatch":
target = getattr(self, "dispatch_target_node", "dev")
self.safe_addstr(self.stdscr, top_y + 2, left_x + 3, f"Target Worker: @{target.upper()} [Tab: Cycle Worker]", self._attr("bold"))
self.safe_addstr(self.stdscr, top_y + 4, left_x + 3, "Enter Build Ticket Title & Goal:", self._attr("dim"))
cog = (self.data.cognitive_cache or {}).get(target) or {}
c_st = cog.get("status") or "IDLE"
c_locked = cog.get("cognitive_lock", False)
lock_label = "LOCKED" if c_locked else "OPEN"
lock_attr = self._attr("danger") if c_locked else self._attr("success")
self.safe_addstr(self.stdscr, top_y + 2, left_x + 3, "CASE ORCHESTRATION & BUILD DISPATCH", self._attr("bold"))
self.safe_addstr(self.stdscr, top_y + 3, left_x + 3, f"Target: @{target.upper():<6} [{c_st:<12}] Lock: {lock_label}", lock_attr)
preset_bar = "Presets: [F1: Build] [F2: Bugfix] [F3: Review] [F4: Audit] [Tab: Cycle Worker]"
self.safe_addstr(self.stdscr, top_y + 4, left_x + 3, preset_bar[:modal_w - 6], self._attr("header"))
field_y = top_y + 5
field_w = modal_w - 6
@@ -4203,8 +4257,8 @@ class MuseTUI:
self.safe_addstr(self.stdscr, field_y + 1, left_x + 3, "│ " + self.modal_input_buf.ljust(field_w - 2) + " │", self._attr("normal"))
self.safe_addstr(self.stdscr, field_y + 2, left_x + 3, "└" + "─" * field_w + "┘", self._attr("dim"))
self.safe_addstr(self.stdscr, top_y + modal_h - 4, left_x + 3, "⚡ Dispatches with 45s Synchronous Readback Gate", self._attr("header"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[Enter] Start & Dispatch Ticket [Tab] Cycle Target [Esc] Cancel", self._attr("dim"))
self.safe_addstr(self.stdscr, top_y + modal_h - 4, left_x + 3, "⚡ 45s Synchronous Readback Gate | Autonomous post-dispatch loopback", self._attr("dim"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[Enter] Dispatch Case [Tab] Cycle Target [F1-F4] Load Preset [Esc] Cancel", self._attr("dim"))
try:
self.stdscr.move(field_y + 1, left_x + 5 + self.modal_input_cursor)
@@ -4234,7 +4288,7 @@ class MuseTUI:
clean_p = p.replace("\n", " ").strip()
self.safe_addstr(self.stdscr, p_start_y + idx, left_x + 3, f"• {clean_p[:modal_w - 8]}", self._attr("normal"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[F] Refocus Main Chat [W] Dispatch Work [Esc/q] Close", self._attr("header"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[F] Refocus Main Chat [W] Dispatch Work [P] Profile Menu [Esc/q] Close", self._attr("header"))
elif self.modal == "agent_menu":
node = getattr(self, "selected_menu_node", self.data.active_node)
@@ -4262,7 +4316,7 @@ class MuseTUI:
line_str = f"• {it['title']}: {it.get('detail', '')}{t_str}"
self.safe_addstr(self.stdscr, m_start_y + idx, left_x + 3, line_str[:modal_w - 8], self._attr("normal"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[1-4 / Tab] Switch Tab [Esc/q] Close", self._attr("dim"))
self.safe_addstr(self.stdscr, top_y + modal_h - 2, left_x + 3, "[1-4/Tab] Tab [w] Dispatch [f] Refocus [h] Heal [t] Thoughts [Esc/q] Close", self._attr("header"))
# -----------------------------------------------------------------------
# Keypress & Event Handling
@@ -4871,6 +4925,10 @@ class MuseTUI:
nodes = list(self.data.nodes)
if self.box_tab == 1 and nodes and 0 <= getattr(self, "fleet_sel_idx", 0) < len(nodes):
target_node = nodes[self.fleet_sel_idx]
elif self.box_tab == 7:
all_workers = ["opm", "646", "dev", "pip", "def", "muse", "muse-main"]
s_idx = getattr(self, "work_sel_idx", 0)
target_node = all_workers[s_idx] if 0 <= s_idx < len(all_workers) else getattr(self, "dispatch_target_node", self.data.active_node)
else:
target_node = getattr(self, "dispatch_target_node", self.data.active_node)
@@ -4895,6 +4953,12 @@ class MuseTUI:
elif ch in (ord('h'), ord('H')):
self._async_heal_agent(target_node)
return True
elif self.box_tab == 7 and ch in (ord('m'), ord('M')):
self._async_merge_pr()
return True
elif self.box_tab == 7 and ch in (ord('a'), ord('A')):
self._async_assign_ticket(target_node)
return True
elif ch in (ord('r'), ord('R')):
threading.Thread(target=self.data._fetch_cognitive_and_work, daemon=True).start()
self.set_toast("Refreshing work pipeline & cognitive state...", "info")
@@ -4957,6 +5021,33 @@ class MuseTUI:
self.set_toast(f"Message Context Menu ({r_role})", "info")
return True
# Main Chat / Transcript Fast Orchestration Actions (NORMAL mode):
# 'w': Dispatch Case, 'W': Work Order, 'f': Refocus Main Chat, 't': Thoughts, 'p': Menu, 'h'/'H': Heal
if self.focus_pane == "transcript":
if ch == ord('w'):
self.dispatch_target_node = self.data.active_node
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
return True
elif ch == ord('W'):
self.modal = "work_order"
self.modal_input_buf = ""
self.modal_input_cursor = 0
return True
elif ch == ord('f'):
self._async_refocus_main(self.data.active_node)
return True
elif ch in (ord('t'), ord('T')):
self._open_agent_thoughts(self.data.active_node)
return True
elif ch == ord('p'):
self._open_agent_menu(self.data.active_node)
return True
elif ch in (ord('h'), ord('H')):
self._async_heal_agent(self.data.active_node)
return True
# Fast Agent Search & Select Modal: 'f' or 'F', or '/' when focused on fleet or sidechats
if ch in (ord('f'), ord('F')) or (self.focus_pane in ("fleet", "sidechats", "sidebar") and ch == ord('/')):
self.modal = "agent_search"
@@ -5177,7 +5268,8 @@ class MuseTUI:
# Work Order modal
if ch in (ord('w'), ord('W')):
self.modal = "work_order"
self.modal = "work_dispatch" if ch == ord('w') else "work_order"
self.dispatch_target_node = self.data.active_node
self.modal_input_buf = ""
self.modal_input_cursor = 0
return True
@@ -5203,6 +5295,24 @@ class MuseTUI:
elif ch == ord('3'):
threading.Thread(target=self._async_resolve_approval, args=("deny", cur_node), daemon=True).start()
return True
elif ch in (ord('w'), ord('W')):
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
self.dispatch_target_node = cur_node
return True
elif ch in (ord('f'), ord('F')):
self._async_refocus_main(cur_node)
return True
elif ch in (ord('t'), ord('T')):
self._open_agent_thoughts(cur_node)
return True
elif ch in (ord('p'), ord('P')):
self._open_agent_menu(cur_node)
return True
elif ch in (ord('h'), ord('H')):
self._async_heal_agent(cur_node)
return True
elif ch in (ord('r'), ord('R')):
threading.Thread(target=self._async_resolve_approval, args=("reply", cur_node, "Proceed with standard execution."), daemon=True).start()
return True
@@ -5229,13 +5339,37 @@ class MuseTUI:
# Box mode Fast Actions: Tab 4 (Tmux)
if self.mode == "box" and self.box_tab == 4:
with self.data.lock:
sessions = list(self.data.tmux_cache)
t_node = self.data.active_node
if sessions and 0 <= self.tmux_sel_idx < len(sessions):
s_name = sessions[self.tmux_sel_idx].split(":")[0].strip()
if s_name.startswith("agent-"):
t_node = s_name.replace("agent-", "")
if ch in (ord('x'), ord('X'), ord('d'), ord('D'), ord('K')):
with self.data.lock:
sessions = list(self.data.tmux_cache)
if sessions and 0 <= self.tmux_sel_idx < len(sessions):
sess_name = sessions[self.tmux_sel_idx].split(":")[0].strip()
threading.Thread(target=self._async_kill_tmux, args=(sess_name,), daemon=True).start()
return True
elif ch in (ord('w'), ord('W')):
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
self.dispatch_target_node = t_node
return True
elif ch in (ord('f'), ord('F')):
self._async_refocus_main(t_node)
return True
elif ch in (ord('t'), ord('T')):
self._open_agent_thoughts(t_node)
return True
elif ch in (ord('p'), ord('P')):
self._open_agent_menu(t_node)
return True
elif ch in (ord('h'), ord('H')):
self._async_heal_agent(t_node)
return True
elif ch in (ord('r'), ord('R')):
threading.Thread(target=self.data._fetch_tmux_sessions, daemon=True).start()
self.set_toast("Refreshed tmux background sessions.", "info")
@@ -5769,7 +5903,7 @@ class MuseTUI:
is_context_popup = self.modal in ("chat_context", "agent_context", "message_context")
if is_context_popup:
modal_w = min(56, w - 4)
modal_h = min(17 if self.modal == "chat_context" else (14 if self.modal == "agent_context" else 11), h - 2)
modal_h = min(17 if self.modal == "chat_context" else (19 if self.modal == "agent_context" else 11), h - 2)
anchor_x, anchor_y = getattr(self, "context_popup_pos", (w // 4, h // 3))
top_y = max(1, min(anchor_y, h - modal_h - 2))
left_x = max(1, min(anchor_x, w - modal_w - 2))
@@ -6326,11 +6460,35 @@ class MuseTUI:
"index": 0,
}
self.context_popup_pos = (mx, my)
self.chat_context_sel = 0
self.modal = "chat_context"
self._modal_open_time = time.time()
return True
# Check if clicked on sub-header case actions / agent profile
if getattr(self, "_btn_case_actions_bounds", None):
btn_y, btn_x_start, btn_x_end = self._btn_case_actions_bounds
if my == btn_y:
if mx < btn_x_start:
# Clicked on agent pill / context in sub-header: open profile menu!
self._open_agent_menu(self.data.active_node)
return True
elif btn_x_start <= mx <= btn_x_end:
rel = mx - btn_x_start
if rel < 9: # "[w:Case]"
self.dispatch_target_node = self.data.active_node
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
elif rel < 21: # "[f:Refocus]"
self._async_refocus_main(self.data.active_node)
elif rel < 34: # "[t:Thoughts]"
self._open_agent_thoughts(self.data.active_node)
elif rel < 43: # "[p:Menu]"
self._open_agent_menu(self.data.active_node)
else: # "[h:Heal]"
self._async_heal_agent(self.data.active_node)
return True
# Check if clicked on any visible [↩ Reply] button on message headers
for (msg_y, btn_x_start, btn_x_end, msg_obj) in getattr(self, "_msg_reply_buttons", []):
if my == msg_y and btn_x_start <= mx <= btn_x_end:
@@ -7215,6 +7373,68 @@ class MuseTUI:
self.data._fetch_cognitive_and_work()
threading.Thread(target=_run, daemon=True).start()
def _async_merge_pr(self, pr_num: Optional[int] = None):
"""Verify and merge an open PR into master."""
if pr_num is None:
with self.data.lock:
prs = list(getattr(self.data, "work_prs_cache", []))
open_prs = [p for p in prs if isinstance(p, dict) and not p.get("merged") and (p.get("state") or "").lower() == "open"]
if not open_prs:
self.set_toast("⚠️ No open PRs found to merge.", "warn_banner")
return
target_pr = open_prs[0].get("number")
else:
target_pr = pr_num
self.set_toast(f"⚡ Verifying and merging PR #{target_pr} into master...", "info")
def _run():
try:
cmd = [sys.executable, str(BIN_DIR / "box-work.py"), "merge", str(target_pr)]
res = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
if res.returncode == 0:
self.set_toast(f"✔ Successfully merged PR #{target_pr} into master.", "success")
else:
err = (res.stderr or res.stdout or "Merge failed").strip().splitlines()[-1]
self.set_toast(f"❌ PR #{target_pr} merge error: {err[:45]}", "error")
except Exception as e:
self.set_toast(f"❌ PR #{target_pr} merge exception: {str(e)[:40]}", "error")
self.data._fetch_cognitive_and_work()
threading.Thread(target=_run, daemon=True).start()
def _async_assign_ticket(self, target_node: str, issue_num: Optional[int] = None):
"""Assign an issue ticket to a target worker node."""
if issue_num is None:
with self.data.lock:
issues = list(getattr(self.data, "work_issues_cache", []))
unassigned = [i for i in issues if isinstance(i, dict) and (i.get("state") or "").lower() == "open"]
free_tickets = [i for i in unassigned if not (i.get("assignee") or {}).get("username")]
candidate = free_tickets[0] if free_tickets else (unassigned[0] if unassigned else None)
if not candidate:
self.dispatch_target_node = target_node
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
self.set_toast(f"No open tickets. Enter goal to dispatch new ticket to @{target_node.upper()}.", "info")
return
target_issue = candidate.get("number")
else:
target_issue = issue_num
self.set_toast(f"⚡ Assigning Issue #{target_issue} to @{target_node.upper()}...", "info")
def _run():
try:
cmd = [sys.executable, str(BIN_DIR / "box-work.py"), "assign", str(target_issue), "--to", target_node]
res = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
if res.returncode == 0:
self.set_toast(f"✔ Issue #{target_issue} assigned to @{target_node.upper()}", "success")
else:
err = (res.stderr or res.stdout or "Assignment failed").strip().splitlines()[-1]
self.set_toast(f"⚠️ Issue #{target_issue} warning: {err[:45]}", "warn_banner")
except Exception as e:
self.set_toast(f"❌ Issue assignment error: {str(e)[:40]}", "error")
self.data._fetch_cognitive_and_work()
threading.Thread(target=_run, daemon=True).start()
def _open_agent_thoughts(self, node: str):
"""Open modal streaming live thought paragraphs and screen state."""
self.selected_thought_node = node
@@ -7414,6 +7634,24 @@ class MuseTUI:
self.focus_pane = "sidebar"
self.set_toast(f"Focused sidebar on {node.upper()}", "info")
self.modal = None
elif action_id == "dispatch":
self.dispatch_target_node = node
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
elif action_id == "thoughts":
self._open_agent_thoughts(node)
elif action_id == "menu":
self._open_agent_menu(node)
elif action_id == "refocus":
self._async_refocus_main(node)
self.modal = None
elif action_id == "heal":
self._async_heal_agent(node)
self.modal = None
elif action_id == "loopback":
self._async_run_loopback()
self.modal = None
def _execute_message_action(self, action_id: str):
"""Execute selected contextual action on the targeted message."""
@@ -7542,7 +7780,9 @@ class MuseTUI:
elif self.modal == "agent_context":
actions = getattr(self, "_agent_context_actions", [
("open", "", ""), ("approvals", "", ""), ("tmux", "", ""),
("open", "", ""), ("dispatch", "", ""), ("thoughts", "", ""),
("menu", "", ""), ("refocus", "", ""), ("approvals", "", ""),
("heal", "", ""), ("loopback", "", ""), ("tmux", "", ""),
("wo", "", ""), ("sync", "", ""), ("read_all", "", "")
])
if ch in (27, ord('q'), ord('Q')):
@@ -7563,11 +7803,17 @@ class MuseTUI:
key_map = {
ord('1'): "open", ord('o'): "open", ord('O'): "open",
ord('2'): "approvals", ord('a'): "approvals", ord('A'): "approvals",
ord('3'): "tmux", ord('t'): "tmux", ord('T'): "tmux",
ord('4'): "wo", ord('w'): "wo", ord('W'): "wo",
ord('5'): "sync", ord('s'): "sync", ord('S'): "sync",
ord('6'): "read_all", ord('m'): "read_all", ord('M'): "read_all",
ord('2'): "dispatch", ord('w'): "dispatch",
ord('3'): "thoughts", ord('t'): "thoughts", ord('T'): "thoughts",
ord('4'): "menu", ord('p'): "menu", ord('P'): "menu",
ord('5'): "refocus", ord('f'): "refocus", ord('F'): "refocus",
ord('6'): "approvals", ord('a'): "approvals", ord('A'): "approvals",
ord('7'): "heal", ord('h'): "heal", ord('H'): "heal",
ord('8'): "loopback", ord('l'): "loopback", ord('L'): "loopback",
ord('9'): "tmux", ord('c'): "tmux", ord('C'): "tmux",
ord('0'): "wo", ord('W'): "wo",
ord('s'): "sync", ord('S'): "sync",
ord('m'): "read_all", ord('M'): "read_all",
}
if ch in key_map:
self._execute_agent_action(key_map[ch])
@@ -7779,6 +8025,22 @@ class MuseTUI:
threading.Thread(target=self._async_dispatch_work, args=(val, target), daemon=True).start()
self.modal = None
return True
if ch == curses.KEY_F1:
self.modal_input_buf = "Build: Implement feature specification and tests"
self.modal_input_cursor = len(self.modal_input_buf)
return True
elif ch == curses.KEY_F2:
self.modal_input_buf = "Bugfix: Investigate and resolve error trace"
self.modal_input_cursor = len(self.modal_input_buf)
return True
elif ch == curses.KEY_F3:
self.modal_input_buf = "Review: Code review and verify pull request changes"
self.modal_input_cursor = len(self.modal_input_buf)
return True
elif ch == curses.KEY_F4:
self.modal_input_buf = "Audit: Pre-flight health and security audit"
self.modal_input_cursor = len(self.modal_input_buf)
return True
if ch in (curses.KEY_BACKSPACE, 127, 8):
if self.modal_input_cursor > 0:
self.modal_input_buf = self.modal_input_buf[:self.modal_input_cursor - 1] + self.modal_input_buf[self.modal_input_cursor:]
@@ -7817,6 +8079,9 @@ class MuseTUI:
self.modal_input_buf = ""
self.modal_input_cursor = 0
return True
elif ch in (ord('p'), ord('P')):
self._open_agent_menu(node)
return True
elif ch in (ord('r'), ord('R')):
self._open_agent_thoughts(node)
return True
@@ -7840,6 +8105,23 @@ class MuseTUI:
elif ch in (ord('4'), ord('i'), ord('I')):
self._async_load_menu(node, "identity")
return True
elif ch in (ord('w'), ord('W')):
self.dispatch_target_node = node
self.modal = "work_dispatch"
self.modal_input_buf = ""
self.modal_input_cursor = 0
return True
elif ch in (ord('f'), ord('F')):
self._async_refocus_main(node)
self.modal = None
return True
elif ch in (ord('h'), ord('H')):
self._async_heal_agent(node)
self.modal = None
return True
elif ch in (ord('t'), ord('T')):
self._open_agent_thoughts(node)
return True
elif ch == ord('\t'):
c_idx = tabs.index(cur_tab) if cur_tab in tabs else 0
next_tab = tabs[(c_idx + 1) % len(tabs)]
+155
View File
@@ -4,6 +4,7 @@ import unittest
from unittest.mock import MagicMock, patch
import os
import sys
import curses
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
@@ -138,6 +139,160 @@ class TestBoxTUIHeadless(unittest.TestCase):
app._handle_key(ord('\t'))
mock_load.assert_called_with("pip", "upcoming")
def test_transcript_sub_header_render_and_mouse_click(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="muse")
app.data.active_node = "dev"
app.focus_pane = "transcript"
app._render_transcript(0, 0, 30, 100)
self.assertIsNotNone(app._btn_case_actions_bounds)
btn_y, b_start, b_end = app._btn_case_actions_bounds
# Click [w:Case] (offset +2)
with patch.object(app, "_open_agent_menu") as mock_menu:
handled = app._handle_mouse(b_start + 2, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
self.assertEqual(app.modal, "work_dispatch")
app.modal = None
# Click [f:Refocus] (offset +12)
with patch.object(app, "_async_refocus_main") as mock_refocus:
handled = app._handle_mouse(b_start + 12, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_refocus.assert_called_with("dev")
# Click [t:Thoughts] (offset +25)
with patch.object(app, "_open_agent_thoughts") as mock_thoughts:
handled = app._handle_mouse(b_start + 25, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_thoughts.assert_called_with("dev")
# Click [p:Menu] (offset +38)
with patch.object(app, "_open_agent_menu") as mock_menu:
handled = app._handle_mouse(b_start + 38, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_menu.assert_called_with("dev")
# Click [h:Heal] (offset +46)
with patch.object(app, "_async_heal_agent") as mock_heal:
handled = app._handle_mouse(b_start + 46, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_heal.assert_called_with("dev")
def test_agent_context_twelve_actions_and_hotkeys(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="muse")
app.data.active_node = "646"
app.context_agent = {"node": "646", "status": {}, "index": 0}
app.modal = "agent_context"
app._render_modal(40, 120)
self.assertEqual(len(app._agent_context_actions), 12)
# Hotkey 2 / w: Dispatch
app.modal = "agent_context"
app._handle_key(ord('2'))
self.assertEqual(app.modal, "work_dispatch")
self.assertEqual(app.dispatch_target_node, "646")
# Hotkey 3 / t: Thoughts
app.modal = "agent_context"
with patch.object(app, "_open_agent_thoughts") as mock_th:
app._handle_key(ord('3'))
mock_th.assert_called_with("646")
# Hotkey 4 / p: Menu
app.modal = "agent_context"
with patch.object(app, "_open_agent_menu") as mock_menu:
app._handle_key(ord('4'))
mock_menu.assert_called_with("646")
# Hotkey 5 / f: Refocus
app.modal = "agent_context"
with patch.object(app, "_async_refocus_main") as mock_ref:
app._handle_key(ord('5'))
mock_ref.assert_called_with("646")
# Hotkey 7 / h: Heal
app.modal = "agent_context"
with patch.object(app, "_async_heal_agent") as mock_heal:
app._handle_key(ord('7'))
mock_heal.assert_called_with("646")
# Hotkey 8 / l: Loopback
app.modal = "agent_context"
with patch.object(app, "_async_run_loopback") as mock_loop:
app._handle_key(ord('8'))
mock_loop.assert_called_once()
def test_work_dispatch_presets_f1_to_f4(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="muse")
app.modal = "work_dispatch"
app.modal_input_buf = ""
app.modal_input_cursor = 0
app.dispatch_target_node = "dev"
app._handle_key(curses.KEY_F1)
self.assertEqual(app.modal_input_buf, "Build: Implement feature specification and tests")
app._handle_key(curses.KEY_F2)
self.assertEqual(app.modal_input_buf, "Bugfix: Investigate and resolve error trace")
app._handle_key(curses.KEY_F3)
self.assertEqual(app.modal_input_buf, "Review: Code review and verify pull request changes")
app._handle_key(curses.KEY_F4)
self.assertEqual(app.modal_input_buf, "Audit: Pre-flight health and security audit")
# Enter triggers dispatch
with patch("threading.Thread") as mock_thread:
app._handle_key(10)
mock_thread.assert_called()
self.assertIsNone(app.modal)
def test_main_panel_normal_mode_orchestration_hotkeys(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="muse")
app.data.active_node = "opm"
app.focus_pane = "transcript"
app.editor_mode = "NORMAL"
# 'w' -> work_dispatch
app._handle_key(ord('w'))
self.assertEqual(app.modal, "work_dispatch")
self.assertEqual(app.dispatch_target_node, "opm")
app.modal = None
# 'f' -> refocus
with patch.object(app, "_async_refocus_main") as mock_ref:
app._handle_key(ord('f'))
mock_ref.assert_called_with("opm")
# 't' -> thoughts
with patch.object(app, "_open_agent_thoughts") as mock_th:
app._handle_key(ord('t'))
mock_th.assert_called_with("opm")
# 'p' -> menu
with patch.object(app, "_open_agent_menu") as mock_menu:
app._handle_key(ord('p'))
mock_menu.assert_called_with("opm")
# 'h' / 'H' -> heal
with patch.object(app, "_async_heal_agent") as mock_heal:
app._handle_key(ord('h'))
mock_heal.assert_called_with("opm")
def test_work_pipeline_assign_and_merge_hotkeys(self):
app = muse_tui.MuseTUI(self.mock_stdscr, initial_mode="box", initial_tab="work")
app.box_tab = 7
app.work_sel_idx = 1 # 646 in all_workers
with patch.object(app, "_async_merge_pr") as mock_merge:
app._handle_key(ord('m'))
mock_merge.assert_called_once()
with patch.object(app, "_async_assign_ticket") as mock_assign:
app._handle_key(ord('a'))
mock_assign.assert_called_with("646")
def test_muse_tui_all_tabs_fuzz_dimensions_and_corrupt_data(self):
dimensions = [(8, 20), (12, 40), (24, 80), (50, 160)]