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 →