From 3a7196f56a663c1934bf279c9e937feb6baccf22 Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Wed, 8 Jul 2026 12:31:17 -0700 Subject: [PATCH] fix(cli): keep flow name over nested crews; show paused flow methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh --- lib/cli/src/crewai_cli/crew_run_tui.py | 34 +++++++++++++++-- lib/cli/tests/test_crew_run_tui.py | 52 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index ae66005ad..e07ec6912 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -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: diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 9bdac231f..0172052d9 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -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 →