diff --git a/apps/web/components/kanban-card-content.tsx b/apps/web/components/kanban-card-content.tsx
index 847a434f73..30bf2485d9 100644
--- a/apps/web/components/kanban-card-content.tsx
+++ b/apps/web/components/kanban-card-content.tsx
@@ -3,7 +3,13 @@
import { useState } from "react";
import { CSS, type Transform } from "@dnd-kit/utilities";
import type { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core";
-import { IconAlertCircle, IconArrowsMaximize, IconDots, IconSubtask } from "@tabler/icons-react";
+import {
+ IconAlertCircle,
+ IconArrowsMaximize,
+ IconDots,
+ IconLoader2,
+ IconSubtask,
+} from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { Card, CardContent } from "@kandev/ui/card";
import { Checkbox } from "@kandev/ui/checkbox";
@@ -193,7 +199,11 @@ function KanbanCardActions({
const hasPendingClarificationRequest = useTaskPendingClarification(task.primarySessionId);
const showQuestionIcon = shouldUseQuestionTaskIcon(task.state, hasPendingClarificationRequest);
const showRunningSpinner = shouldShowTaskRunningSpinner(task.state, task.primarySessionState);
- const statusIcon = getTaskStateIcon(task.state, "h-4 w-4", hasPendingClarificationRequest);
+ const statusIcon = showRunningSpinner ? (
+
+ ) : (
+ getTaskStateIcon(task.state, "h-4 w-4", hasPendingClarificationRequest)
+ );
const hasKnownSession =
Boolean(task.primarySessionId) || Boolean(task.sessionCount && task.sessionCount > 0);
diff --git a/apps/web/lib/ui/state-icons.test.tsx b/apps/web/lib/ui/state-icons.test.tsx
index e66d9b4867..c37ef566ed 100644
--- a/apps/web/lib/ui/state-icons.test.tsx
+++ b/apps/web/lib/ui/state-icons.test.tsx
@@ -23,13 +23,19 @@ describe("getTaskStateIcon", () => {
});
describe("shouldShowTaskRunningSpinner", () => {
- it("returns false for non-loading task states regardless of session state", () => {
+ it("returns false for non-loading task states without an active session", () => {
expect(shouldShowTaskRunningSpinner("COMPLETED")).toBe(false);
expect(shouldShowTaskRunningSpinner("FAILED")).toBe(false);
expect(shouldShowTaskRunningSpinner("CANCELLED")).toBe(false);
expect(shouldShowTaskRunningSpinner("REVIEW")).toBe(false);
expect(shouldShowTaskRunningSpinner("TODO")).toBe(false);
- expect(shouldShowTaskRunningSpinner("REVIEW", "RUNNING")).toBe(false);
+ });
+
+ it("returns true when any task state has an actively running primary session", () => {
+ expect(shouldShowTaskRunningSpinner("REVIEW", "RUNNING")).toBe(true);
+ expect(shouldShowTaskRunningSpinner("COMPLETED", "RUNNING")).toBe(true);
+ expect(shouldShowTaskRunningSpinner("FAILED", "RUNNING")).toBe(true);
+ expect(shouldShowTaskRunningSpinner("CANCELLED", "RUNNING")).toBe(true);
});
it("returns true for SCHEDULING with no primary session yet", () => {
diff --git a/apps/web/lib/ui/state-icons.tsx b/apps/web/lib/ui/state-icons.tsx
index c7c6f3e027..303f4021e9 100644
--- a/apps/web/lib/ui/state-icons.tsx
+++ b/apps/web/lib/ui/state-icons.tsx
@@ -83,11 +83,11 @@ const ACTIVE_SESSION_STATES: ReadonlySet = new Set([
]);
/**
- * Returns true when the kanban card should show the spinning loader for an
- * in-progress task. The task workflow state and the primary session's runtime
- * state are decoupled — the workflow can keep a task in `IN_PROGRESS` after
- * the agent has finished, errored, or paused waiting for input — so we gate
- * the spinner on the primary session being actively running.
+ * Returns true when the kanban card should show the spinning loader. The task
+ * workflow state and the primary session's runtime state are decoupled — the
+ * workflow can keep a task in `IN_PROGRESS` after the agent has finished, or
+ * move it to `REVIEW` while the current primary session is still running — so
+ * an explicit primary session state takes precedence.
*
* When no primary session is attached yet (task just created / scheduling),
* we still show the spinner so users see the imminent work; otherwise we
@@ -97,9 +97,8 @@ export function shouldShowTaskRunningSpinner(
taskState?: TaskState,
primarySessionState?: string | null,
): boolean {
- if (taskState !== "IN_PROGRESS" && taskState !== "SCHEDULING") return false;
- if (!primarySessionState) return true;
- return ACTIVE_SESSION_STATES.has(primarySessionState);
+ if (primarySessionState) return ACTIVE_SESSION_STATES.has(primarySessionState);
+ return taskState === "IN_PROGRESS" || taskState === "SCHEDULING";
}
export function shouldUsePermissionTaskIcon(hasPendingPermission = false): boolean {