Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions strix/interface/assets/tui_styles.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,76 @@ StopAgentScreen {
border: none;
}

WrapUpAgentScreen {
align: center middle;
background: $background 0%;
}

#wrap_up_agent_dialog {
grid-size: 1;
grid-gutter: 1;
grid-rows: auto auto auto;
padding: 1;
width: 40;
height: auto;
border: round #f59e0b;
background: #1a1a1a 98%;
}

#wrap_up_agent_title {
color: #f59e0b;
text-style: bold;
text-align: center;
width: 100%;
margin-bottom: 0;
}

#wrap_up_agent_description {
color: #a3a3a3;
text-align: center;
width: 100%;
margin-bottom: 0;
}

#wrap_up_agent_buttons {
grid-size: 2;
grid-gutter: 1;
grid-columns: 1fr 1fr;
width: 100%;
height: 1;
}

#wrap_up_agent_buttons Button {
height: 1;
min-height: 1;
border: none;
text-style: bold;
}

#wrap_up_agent {
background: transparent;
color: #f59e0b;
border: none;
}

#wrap_up_agent:hover, #wrap_up_agent:focus {
background: #f59e0b;
color: #1a1a1a;
border: none;
}

#cancel_wrap_up {
background: transparent;
color: #737373;
border: none;
}

#cancel_wrap_up:hover, #cancel_wrap_up:focus {
background: rgb(54, 54, 54);
color: #ffffff;
border: none;
}

QuitScreen {
align: center middle;
background: $background 0%;
Expand Down
120 changes: 119 additions & 1 deletion strix/interface/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ def compose(self) -> ComposeResult:
yield Grid(
Label("🦉 Strix Help", id="help_title"),
Label(
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n"
"F1 Help\nCtrl+Q/C Quit\nESC Stop Agent (hard stop)\n"
"W Wrap Up Agent (graceful)\n"
"Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree",
id="help_content",
),
Expand Down Expand Up @@ -217,6 +218,59 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
self.app.pop_screen()


class WrapUpAgentScreen(ModalScreen): # type: ignore[misc]
def __init__(self, agent_name: str, agent_id: str):
super().__init__()
self.agent_name = agent_name
self.agent_id = agent_id

def compose(self) -> ComposeResult:
yield Grid(
Label(f"📝 Wrap up '{self.agent_name}'?", id="wrap_up_agent_title"),
Label(
"Agent will finish current work and report findings.",
id="wrap_up_agent_description",
),
Grid(
Button("Yes", variant="warning", id="wrap_up_agent"),
Button("No", variant="default", id="cancel_wrap_up"),
id="wrap_up_agent_buttons",
),
id="wrap_up_agent_dialog",
)

def on_mount(self) -> None:
cancel_button = self.query_one("#cancel_wrap_up", Button)
cancel_button.focus()

def on_key(self, event: events.Key) -> None:
if event.key in ("left", "right", "up", "down"):
focused = self.focused

if focused and focused.id == "wrap_up_agent":
cancel_button = self.query_one("#cancel_wrap_up", Button)
cancel_button.focus()
else:
wrap_up_button = self.query_one("#wrap_up_agent", Button)
wrap_up_button.focus()

event.prevent_default()
elif event.key == "enter":
focused = self.focused
if focused and isinstance(focused, Button):
focused.press()
event.prevent_default()
elif event.key == "escape":
self.app.pop_screen()
event.prevent_default()

def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "wrap_up_agent":
self.app.action_confirm_wrap_up_agent(self.agent_id)
else:
self.app.pop_screen()


class QuitScreen(ModalScreen): # type: ignore[misc]
def compose(self) -> ComposeResult:
yield Grid(
Expand Down Expand Up @@ -272,6 +326,7 @@ class StrixTUIApp(App): # type: ignore[misc]
Binding("ctrl+q", "request_quit", "Quit", priority=True),
Binding("ctrl+c", "request_quit", "Quit", priority=True),
Binding("escape", "stop_selected_agent", "Stop Agent", priority=True),
Binding("w", "wrap_up_selected_agent", "Wrap Up Agent", priority=True),
]

def __init__(self, args: argparse.Namespace):
Expand Down Expand Up @@ -1227,6 +1282,69 @@ def action_confirm_stop_agent(self, agent_id: str) -> None:

logging.exception(f"Failed to stop agent {agent_id}")

def action_wrap_up_selected_agent(self) -> None:
if (
self.show_splash
or not self.is_mounted
or len(self.screen_stack) > 1
or not self.selected_agent_id
):
return

agent_name, should_wrap_up = self._validate_agent_for_wrap_up()
if not should_wrap_up:
return

try:
self.query_one("#main_container")
except (ValueError, Exception):
return

self.push_screen(WrapUpAgentScreen(agent_name, self.selected_agent_id))

def _validate_agent_for_wrap_up(self) -> tuple[str, bool]:
agent_name = "Unknown Agent"

try:
if self.tracer and self.selected_agent_id in self.tracer.agents:
agent_data = self.tracer.agents[self.selected_agent_id]
agent_name = agent_data.get("name", "Unknown Agent")

agent_status = agent_data.get("status", "running")
if agent_status not in ["running", "waiting"]:
return agent_name, False

return agent_name, True

except (KeyError, AttributeError, ValueError) as e:
import logging

logging.warning(f"Failed to validate agent for wrap-up: {e}")

return agent_name, False

def action_confirm_wrap_up_agent(self, agent_id: str) -> None:
self.pop_screen()

try:
from strix.tools.agents_graph.agents_graph_actions import wrap_up_agent

result = wrap_up_agent(agent_id)

import logging

if result.get("success"):
logging.info(f"Wrap-up request sent to agent: {result.get('message', 'Unknown')}")
else:
logging.warning(
f"Failed to send wrap-up request: {result.get('error', 'Unknown error')}"
)

except Exception:
import logging

logging.exception(f"Failed to wrap up agent {agent_id}")

def action_custom_quit(self) -> None:
for agent_id in list(self._agent_verb_timers.keys()):
self._stop_agent_verb_timer(agent_id)
Expand Down
76 changes: 76 additions & 0 deletions strix/tools/agents_graph/agents_graph_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,82 @@ def stop_agent(agent_id: str) -> dict[str, Any]:
}


def wrap_up_agent(agent_id: str) -> dict[str, Any]:
"""Request an agent to gracefully wrap up its work and report findings."""
try:
if agent_id not in _agent_graph["nodes"]:
return {
"success": False,
"error": f"Agent '{agent_id}' not found in graph",
"agent_id": agent_id,
}

agent_node = _agent_graph["nodes"][agent_id]

if agent_node["status"] in ["completed", "error", "failed", "stopped"]:
return {
"success": True,
"message": f"Agent '{agent_node['name']}' has already finished",
"agent_id": agent_id,
"previous_status": agent_node["status"],
}

# Send a wrap-up message to the agent
wrap_up_message = """<wrap_up_request>
<priority>URGENT</priority>
<instruction>
The user has requested that you wrap up your current work.
You have approximately 3-5 iterations to:
1. Complete or pause any critical in-progress work
2. Summarize your findings and progress so far
3. Call agent_finish (if you are a subagent) or finish_scan (if you are the root agent)
to properly report your results

Do NOT start any new major tasks. Focus on concluding your work gracefully.
</instruction>
</wrap_up_request>"""

if agent_id not in _agent_messages:
_agent_messages[agent_id] = []

from uuid import uuid4

_agent_messages[agent_id].append(
{
"id": f"wrapup_{uuid4().hex[:8]}",
"from": "user",
"to": agent_id,
"content": wrap_up_message,
"message_type": "instruction",
"priority": "urgent",
"timestamp": datetime.now(UTC).isoformat(),
"delivered": True,
"read": False,
}
)

# If agent is waiting, resume it so it can process the wrap-up request
if agent_id in _agent_states:
agent_state = _agent_states[agent_id]
if agent_state.is_waiting_for_input():
agent_state.resume_from_waiting()

return {
"success": True,
"message": f"Wrap-up request sent to agent '{agent_node['name']}'",
"agent_id": agent_id,
"agent_name": agent_node["name"],
"note": "Agent will gracefully finish and report its findings",
}

except Exception as e: # noqa: BLE001
return {
"success": False,
"error": f"Failed to send wrap-up request: {e}",
"agent_id": agent_id,
}


def send_user_message_to_agent(agent_id: str, message: str) -> dict[str, Any]:
try:
if agent_id not in _agent_graph["nodes"]:
Expand Down