feat(ux): complete multi-panel case orchestration, presets, and main panel cognitive status
This commit is contained in:
+310
-28
@@ -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)]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user