Files
kandev/apps/web
João Salavisa f8654880f3 fix(kanban): stop stuck spinner by writing task.state=REVIEW on engine on_turn_complete (#1001)
* fix(web): stop kanban spinner when primary session is terminal

The kanban card spinner was gated only on `task.state` being IN_PROGRESS
or SCHEDULING. Workflows leave a task in the IN_PROGRESS column after
the agent's session reaches a terminal state (COMPLETED/FAILED/CANCELLED)
so the user can review and move it manually, which made the loading
icon spin indefinitely.

The frontend already receives `primary_session_state` over SSR and WS;
gate the spinner on that not being terminal. Fixes #985.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(orchestrator): write task.state=REVIEW after engine on_turn_complete transition

The non-engine path (setSessionWaitingForInput → writeTaskReviewState) has
always written tasks.state = REVIEW when the agent's session paused for
input. The workflow-engine on_turn_complete path forgot the task-state
write: it persisted the new workflow step + flipped the session to
WAITING_FOR_INPUT but left tasks.state stale at IN_PROGRESS. That left
the kanban card stuck in IN_PROGRESS — with a spinning loader — even
after the workflow had moved it into the "Review" column.

Mirror the non-engine path: call writeTaskReviewState inside the
RUNNING/STARTING branch of applyEngineTransition. Add a regression test
that asserts tasks.state = REVIEW after an engine-driven on_turn_complete
transition.

Also tighten the frontend gate added in the previous commit to suppress
the spinner for any non-active primary-session state (WAITING_FOR_INPUT,
IDLE in addition to COMPLETED/FAILED/CANCELLED). The backend fix is the
real correction; the broader frontend gate stays as defense-in-depth
against future state-sync drift.

Refs #985.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(orchestrator): drop redundant time.Sleep in engine on_turn_complete test

writeTaskReviewState and ApplyTransition both run synchronously inside
applyEngineTransition (before processOnEnter's goroutine), so the
task-state and step assertions need no async wait. assertSessionState
already has its own 500ms polling loop for the WAITING_FOR_INPUT flip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:58:14 +01:00
..