mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-09 17:05:08 +00:00
fix(cli): keep flow name over nested crews; show paused flow methods
Two review follow-ups on the flow TUI: 1) Crew kickoff renamed the flow (Cursor): CrewKickoffStartedEvent overwrote _crew_name / the app title with a nested `call: crew` step's crew name, so the post-run summary could be labeled with a child crew. The rename is now gated on `not _is_flow_run`, preserving the flow's name; crew runs still adopt the crew name. Tests: test_crew_kickoff_does_not_rename_flow_run, test_crew_kickoff_renames_in_crew_mode. 2) Paused methods showed active (Cursor): the TUI didn't handle MethodExecutionPausedEvent, so a @human_feedback pause left the STEPS spinner running (flow status panels are suppressed in TUI mode). It now marks the step "paused" (⏸, teal) and the header shows "waiting for feedback" instead of a spinner. Test: test_method_paused_marks_step_paused. Note: interactively *providing* human feedback from the flow TUI is a separate follow-up; this only makes the pause visible instead of a silent stuck spinner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
This commit is contained in:
@@ -1242,6 +1242,9 @@ FooterKey .footer-key--key {
|
||||
elif status == "failed":
|
||||
t.append(" ✘ ", style=_C_RED)
|
||||
t.append(name, style=_C_RED)
|
||||
elif status == "paused":
|
||||
t.append(" ⏸ ", style=_C_TEAL)
|
||||
t.append(name, style=_C_TEAL)
|
||||
else:
|
||||
t.append(" ○ ", style=_C_DIM)
|
||||
t.append(name, style=_C_DIM)
|
||||
@@ -1347,12 +1350,22 @@ FooterKey .footer-key--key {
|
||||
if self._error:
|
||||
t.append(f"\n{self._error[:120]}", style=_C_RED)
|
||||
elif self._current_method:
|
||||
t.append(f"{self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append(self._current_method, style=f"bold {_C_PRIMARY}")
|
||||
paused = any(
|
||||
s["name"] == self._current_method and s["status"] == "paused"
|
||||
for s in self._flow_steps
|
||||
)
|
||||
if paused:
|
||||
t.append("⏸ ", style=_C_TEAL)
|
||||
t.append(self._current_method, style=f"bold {_C_TEAL}")
|
||||
else:
|
||||
t.append(f"{self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append(self._current_method, style=f"bold {_C_PRIMARY}")
|
||||
call_type = self._flow_method_types.get(self._current_method)
|
||||
if call_type:
|
||||
t.append(f" ({call_type})", style=_C_DIM)
|
||||
if self._current_agent:
|
||||
if paused:
|
||||
t.append(" waiting for feedback", style=_C_DIM)
|
||||
elif self._current_agent:
|
||||
t.append("\nAgent: ", style=_C_DIM)
|
||||
t.append(self._current_agent, style=f"bold {_C_TEXT}")
|
||||
else:
|
||||
@@ -1984,6 +1997,7 @@ FooterKey .footer-key--key {
|
||||
FlowStartedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionPausedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.llm_events import (
|
||||
@@ -2019,7 +2033,9 @@ FooterKey .footer-key--key {
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
with self._lock:
|
||||
if event.crew_name:
|
||||
# In flow mode the app is named for the flow; a nested crew's
|
||||
# kickoff (a `call: crew` step) must not rename it.
|
||||
if event.crew_name and not self._is_flow_run:
|
||||
self._crew_name = event.crew_name
|
||||
self.title = f"CrewAI — {event.crew_name}"
|
||||
self._status = "working"
|
||||
@@ -2075,6 +2091,16 @@ FooterKey .footer-key--key {
|
||||
|
||||
self._register_handler(MethodExecutionFailedEvent, on_method_failed)
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionPausedEvent)
|
||||
def on_method_paused(source: Any, event: MethodExecutionPausedEvent) -> None:
|
||||
# A @human_feedback method paused; flow status panels are suppressed
|
||||
# in TUI mode, so surface the wait in STEPS/header instead of leaving
|
||||
# a spinner. _current_method stays pointed at it.
|
||||
with self._lock:
|
||||
self._set_flow_step_status(event.method_name, "paused")
|
||||
|
||||
self._register_handler(MethodExecutionPausedEvent, on_method_paused)
|
||||
|
||||
@crewai_event_bus.on(TaskStartedEvent)
|
||||
def on_task_started(source: Any, event: TaskStartedEvent) -> None:
|
||||
with self._lock:
|
||||
|
||||
@@ -6,10 +6,12 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.crew_events import CrewKickoffStartedEvent
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowStartedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionPausedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.memory_events import (
|
||||
@@ -1614,6 +1616,56 @@ def test_flow_method_transitions_clear_current_agent() -> None:
|
||||
app._unsubscribe()
|
||||
|
||||
|
||||
def test_crew_kickoff_does_not_rename_flow_run() -> None:
|
||||
# A `call: crew` step must not relabel the flow with the nested crew's name.
|
||||
app = CrewRunApp(crew_name="My Flow")
|
||||
app._flow = SimpleNamespace()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(CrewKickoffStartedEvent(crew_name="Nested Crew", inputs=None))
|
||||
assert app._crew_name == "My Flow"
|
||||
assert app._status == "working"
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
|
||||
def test_crew_kickoff_renames_in_crew_mode() -> None:
|
||||
# Regression: crew runs still adopt the crew name from the event.
|
||||
app = CrewRunApp(crew_name="Crew")
|
||||
app._crew = SimpleNamespace()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(CrewKickoffStartedEvent(crew_name="Real Crew", inputs=None))
|
||||
assert app._crew_name == "Real Crew"
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
|
||||
def test_method_paused_marks_step_paused() -> None:
|
||||
app = CrewRunApp(crew_name="Demo")
|
||||
app._flow = SimpleNamespace()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
MethodExecutionStartedEvent(flow_name="Demo", method_name="ask", state={})
|
||||
)
|
||||
_emit_event(
|
||||
MethodExecutionPausedEvent(
|
||||
flow_name="Demo",
|
||||
method_name="ask",
|
||||
state={},
|
||||
flow_id="flow-1",
|
||||
message="Need your input",
|
||||
)
|
||||
)
|
||||
assert app._flow_steps == [
|
||||
{"name": "ask", "call_type": None, "status": "paused"}
|
||||
]
|
||||
assert app._current_method == "ask"
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_declarative_flow_runs_on_tui() -> None:
|
||||
"""End-to-end: on_mount dispatches _run_flow_worker → flow.kickoff →
|
||||
|
||||
Reference in New Issue
Block a user