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 {