From b6135f4bd5fd28e25bf49911819f6c6bec74df37 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 22 Jul 2026 13:56:53 -0700 Subject: [PATCH] FN-8494: keep task cards active while replanning Keep task-card activity chrome visible throughout durable and fresh replanning states. - Treat needs-replan tasks as agent-active on triage and todo lanes without changing lock policy. - Cover board, list, mobile, pause, and freshness behaviors with regression tests. - Add a patch changeset for the replanning activity indicator. Files changed: .changeset/fn-8494-replan-active-glow.md | 7 +++++ .../app/components/__tests__/ListView.test.tsx | 4 +++ .../app/components/__tests__/TaskCard.test.tsx | 31 ++++++++++++++++++++++ .../app/utils/__tests__/taskActivity.test.ts | 23 ++++++++++++++++ packages/dashboard/app/utils/taskActivity.ts | 7 ++++- 5 files changed, 71 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8494 Fusion-Task-Lineage: a910a22a-cff7-423f-82ee-359830beb104 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8494-replan-active-glow.md | 7 +++++ .../components/__tests__/ListView.test.tsx | 4 +++ .../components/__tests__/TaskCard.test.tsx | 31 +++++++++++++++++++ .../app/utils/__tests__/taskActivity.test.ts | 23 ++++++++++++++ packages/dashboard/app/utils/taskActivity.ts | 7 ++++- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-8494-replan-active-glow.md diff --git a/.changeset/fn-8494-replan-active-glow.md b/.changeset/fn-8494-replan-active-glow.md new file mode 100644 index 0000000000..7d75e9c7c9 --- /dev/null +++ b/.changeset/fn-8494-replan-active-glow.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep task-card active glow during replan and revise while agents work. +category: fix +dev: isTaskAgentActive treats needs-replan (and plan-in-place replan freshness) as agent-active for board/list chrome; lock policy documented in taskActivity FNXC. diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx index 6e83783a76..21dd81c0a2 100644 --- a/packages/dashboard/app/components/__tests__/ListView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx @@ -2248,6 +2248,8 @@ describe("ListView", () => { it.each([ { status: "executing", column: "in-progress" as const, label: "executing" }, { status: "merging-fix", column: "in-review" as const, label: "Merging fixes…" }, + { status: "needs-replan", column: "triage" as const, label: "Revising" }, + { status: "needs-replan", column: "todo" as const, label: "Revising" }, ])("renders agent-active tasks with static highlight styling for $status", ({ status, column, label }) => { const tasks = [ createMockTask({ @@ -5378,6 +5380,8 @@ describe("ListView - Bulk Selection", () => { it.each([ { status: "executing", column: "in-progress" as const }, { status: "merging-fix", column: "in-review" as const }, + { status: "needs-replan", column: "triage" as const }, + { status: "needs-replan", column: "todo" as const }, ])("applies agent-active class to mobile cards for active states (%s)", ({ status, column }) => { mockMobileViewport(); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index df64e5f13d..c1dcaea267 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -2651,6 +2651,37 @@ describe("TaskCard", () => { expect(container.querySelector(".card-status-badge")).toBeNull(); }); + it("keeps board replan cards glowing and their status badge pulsing", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".card")).toHaveClass("agent-active"); + expect(screen.getByText("Revising")).toHaveClass("card-status-badge", "pulsing"); + }); + + it.each([ + ["global pause", { globalPaused: true }, {}], + ["render queue", { queued: true }, {}], + ["task pause", {}, { paused: true }], + ])("suppresses board replan activity during $name", (_name, props, taskOverrides) => { + const { container } = render( + , + ); + + expect(container.querySelector(".card")).not.toHaveClass("agent-active"); + expect(container.querySelector(".card-status-badge")).not.toHaveClass("pulsing"); + }); + /* * FNXC:ReleaseAuthorizationGate 2026-07-09-00:00: the triage release-authorization * gate was removed. A legacy release-authorization hold now renders the generic diff --git a/packages/dashboard/app/utils/__tests__/taskActivity.test.ts b/packages/dashboard/app/utils/__tests__/taskActivity.test.ts index e07cbe2799..a02b34588b 100644 --- a/packages/dashboard/app/utils/__tests__/taskActivity.test.ts +++ b/packages/dashboard/app/utils/__tests__/taskActivity.test.ts @@ -48,6 +48,12 @@ describe("isTaskAgentActive", () => { expect(isTaskAgentActive(taskWithRunningWorkflowStep())).toBe(true); }); + it("keeps durable replan cards active without changing lock statuses", () => { + expect(ACTIVE_STATUSES.has("needs-replan")).toBe(false); + expect(isTaskAgentActive(makeTask({ status: "needs-replan", column: "triage" }))).toBe(true); + expect(isTaskAgentActive(makeTask({ status: "needs-replan", column: "todo" }))).toBe(true); + }); + it("does not treat a status-null task without a running item as active", () => { expect(isTaskAgentActive(makeTask())).toBe(false); }); @@ -64,6 +70,17 @@ describe("isTaskAgentActive", () => { }))).toBe(false); }); + it("extends fresh planner activity to plan-in-place todo replans", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-22T09:25:30.000Z")); + + expect(isTaskAgentActive(makeTask({ + column: "todo", + status: "needs-replan", + recentAgentActivityAt: "2026-07-22T09:25:00.000Z", + }))).toBe(true); + }); + it.each([ ["queued task status", taskWithRunningWorkflowStep({ status: "queued" }), {}], ["paused status", taskWithRunningWorkflowStep({ status: "paused" }), {}], @@ -85,6 +102,12 @@ describe("isTaskAgentActive", () => { ["archived column with fresh planner log", makeTask({ column: "archived", recentAgentActivityAt: new Date().toISOString() }), {}], ["awaiting approval with fresh planner log", makeTask({ status: "awaiting-approval", recentAgentActivityAt: new Date().toISOString() }), {}], ["awaiting user input with fresh planner log", makeTask({ status: "awaiting-user-input", recentAgentActivityAt: new Date().toISOString() }), {}], + ["queued replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { queued: true }], + ["derived stuck replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { isStuck: true }], + ["global pause replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { globalPaused: true }], + ["paused replan", makeTask({ status: "needs-replan", paused: true, recentAgentActivityAt: new Date().toISOString() }), {}], + ["failed replan", makeTask({ status: "failed", recentAgentActivityAt: new Date().toISOString() }), {}], + ["done-column replan", makeTask({ column: "done", status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), {}], ] as const)("rejects %s before running workflow activity", (_name, task, options) => { expect(isTaskAgentActive(task, options)).toBe(false); }); diff --git a/packages/dashboard/app/utils/taskActivity.ts b/packages/dashboard/app/utils/taskActivity.ts index ef76c30c1e..47bc2281a8 100644 --- a/packages/dashboard/app/utils/taskActivity.ts +++ b/packages/dashboard/app/utils/taskActivity.ts @@ -29,6 +29,9 @@ FN-8055 makes the agent-active border and pulsing badges represent the same grou FNXC:TaskActivity 2026-07-28-12:00: FN-8300 also honors a bounded, client-only fresh planner-log timestamp for triage cards. The log stream can arrive before the authoritative planning-status row; this render-only fallback closes that window without changing routing/model locks. +FNXC:TaskActivity 2026-07-22-09:25: +FN-8494 requires cards parked in the engine's durable `needs-replan` planning stage to keep their activity chrome on both triage and plan-in-place todo lanes. This is rendering-only: do not add `needs-replan` to ACTIVE_STATUSES, because model and routing pickers use that set as a long-lived lock policy while this predicate only describes live operator chrome. Extend the bounded fresh-log window to the todo replan lane so an incoming planner log remains represented consistently there. + Stuck-killed and both terminal columns are never active, even when stale execution status or workflow-step data remains on the task. Model-resolution and routing locks intentionally import only ACTIVE_STATUSES and retain their status-or-in-progress policy; using this rendering predicate there would change lock behavior during status-null workflow steps. @@ -58,15 +61,17 @@ export function isTaskAgentActive( return false; } + const isReplanning = status === "needs-replan"; const recentPlannerActivityAtMs = Date.parse(task.recentAgentActivityAt ?? ""); const nowMs = Date.now(); - const hasFreshPlannerActivity = task.column === "triage" + const hasFreshPlannerActivity = (task.column === "triage" || (task.column === "todo" && isReplanning)) && Number.isFinite(recentPlannerActivityAtMs) && nowMs - recentPlannerActivityAtMs >= 0 && nowMs - recentPlannerActivityAtMs <= RECENT_PLANNER_ACTIVITY_WINDOW_MS; return task.column === "in-progress" || ACTIVE_STATUSES.has(status ?? "") || + isReplanning || hasFreshPlannerActivity || getUnifiedTaskProgress(task).items.some((item) => item.status === "running"); }