diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index 35767cdb6..8fe08b456 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -725,6 +725,19 @@ FooterKey .footer-key--key { step["status"] = status return + def _clear_current_method(self, finished_name: str) -> None: + """Drop the header's active method once it ends. Caller holds the lock. + + Falls back to another still-active step (methods can overlap) so the + header never keeps spinning a method the STEPS list already shows as + done or failed. + """ + if self._current_method != finished_name: + return + self._current_method = next( + (s["name"] for s in self._flow_steps if s["status"] == "active"), None + ) + def _on_crew_done(self, output: str | None) -> None: with self._lock: self._status = "completed" @@ -1341,7 +1354,12 @@ FooterKey .footer-key--key { t.append(self._current_agent, style=f"bold {_C_TEXT}") else: t.append(f"{self._spinner()} ", style=_C_PRIMARY) - t.append("Starting flow…", style=_C_DIM) + # "Working…" once a step has run (between/after methods); + # "Starting flow…" only before the first method. + t.append( + "Working…" if self._flow_steps else "Starting flow…", + style=_C_DIM, + ) widget.update(t) return @@ -2039,6 +2057,7 @@ FooterKey .footer-key--key { ) -> None: with self._lock: self._set_flow_step_status(event.method_name, "done") + self._clear_current_method(event.method_name) self._register_handler(MethodExecutionFinishedEvent, on_method_finished) @@ -2046,6 +2065,7 @@ FooterKey .footer-key--key { def on_method_failed(source: Any, event: MethodExecutionFailedEvent) -> None: with self._lock: self._set_flow_step_status(event.method_name, "failed") + self._clear_current_method(event.method_name) self._register_handler(MethodExecutionFailedEvent, on_method_failed) diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 489ea1358..90c4da562 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -1550,6 +1550,40 @@ def test_flow_method_events_build_steps() -> None: {"name": "research", "call_type": "crew", "status": "done"}, {"name": "summarize", "call_type": "agent", "status": "failed"}, ] + # The header must not keep spinning a method that already ended. + assert app._current_method is None + + +def test_current_method_clears_and_falls_back_across_overlap() -> None: + app = CrewRunApp(crew_name="Demo") + app._flow = SimpleNamespace() + app._subscribe() + try: + _emit_event( + MethodExecutionStartedEvent(flow_name="Demo", method_name="a", state={}) + ) + _emit_event( + MethodExecutionStartedEvent(flow_name="Demo", method_name="b", state={}) + ) + assert app._current_method == "b" + + # 'a' finishes while 'b' is still active → header stays on 'b'. + _emit_event( + MethodExecutionFinishedEvent( + flow_name="Demo", method_name="a", result=None, state={} + ) + ) + assert app._current_method == "b" + + # 'b' finishes → nothing active left → header clears. + _emit_event( + MethodExecutionFinishedEvent( + flow_name="Demo", method_name="b", result=None, state={} + ) + ) + assert app._current_method is None + finally: + app._unsubscribe() @pytest.mark.asyncio