fix(cli): clear flow header's current method when a method ends

Review follow-up: the flow header keys off _current_method, which was set on
MethodExecutionStarted but never cleared on Finished/Failed. Between steps (or
after a failed method before kickoff exits) the header kept spinning the old
method name while the STEPS sidebar already showed it done/failed.

_clear_current_method now drops the header's active method when it ends,
falling back to another still-active step (methods can overlap) or none. The
header's idle fallback shows "Working…" once a step has run and "Starting
flow…" only before the first method.

Tests: test_current_method_clears_and_falls_back_across_overlap, plus a
_current_method assertion in test_flow_method_events_build_steps.

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:
Joao Moura
2026-07-08 07:47:07 -07:00
parent 6b7529f2d9
commit ec55c31fed
2 changed files with 55 additions and 1 deletions

View File

@@ -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)

View File

@@ -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