Files
fusion/packages/dashboard/app/utils/taskActivity.ts
gsxdsm 9debeaa951 FN-8300: show planner activity on status-null cards
Keep planning cards visibly active while fresh planner logs precede authoritative task status updates.

- Track bounded client-only planner activity from fresh triage log events
- Render matching pulsing Planning badges in board and list card views
- Cover transient activity, authoritative clearing, and inactive edge cases

Files changed:
 docs/dashboard-guide.md                            |  2 +
 packages/core/src/types.ts                         |  6 +++
 packages/dashboard/app/components/ListView.tsx     | 26 ++++++++---
 packages/dashboard/app/components/TaskCard.tsx     | 20 +++++---
 .../app/components/__tests__/ListView.test.tsx     | 38 +++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 19 +++++++-
 .../dashboard/app/hooks/__tests__/useTasks.test.ts | 54 ++++++++++++++++++++++
 packages/dashboard/app/hooks/useTasks.ts           | 44 +++++++++++++-----
 .../app/utils/__tests__/taskActivity.test.ts       | 22 ++++++++-
 packages/dashboard/app/utils/taskActivity.ts       | 15 +++++-
 10 files changed, 219 insertions(+), 27 deletions(-)

Fusion-Task-Id: FN-8300

Fusion-Task-Lineage: e12f1277-5628-45a1-b731-54310027540e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 12:13:20 -07:00

73 lines
2.8 KiB
TypeScript

import type { Task } from "@fusion/core";
import { getUnifiedTaskProgress } from "./taskProgress";
/** The shared status vocabulary for active task phases and lock/model policy. */
export const ACTIVE_STATUSES = new Set([
"planning",
"researching",
"executing",
"finalizing",
"merging",
"merging-pr",
"merging-fix",
"reviewing",
"landing",
]);
export const RECENT_PLANNER_ACTIVITY_WINDOW_MS = 60_000;
export interface TaskAgentActivityOptions {
globalPaused?: boolean;
queued?: boolean;
isStuck?: boolean;
}
/*
FNXC:TaskActivity 2026-07-16-00:00:
FN-8055 makes the agent-active border and pulsing badges represent the same ground truth: an agent is working now. Reject render-context global pause, queue, and derived freshness-stuck gates before checking activity, then combine the engine's column-aware active window with canonical phase statuses and the running unified workflow item that drives progress badges.
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.
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.
*/
export function isTaskAgentActive(
task: Pick<Task, "column" | "status" | "paused" | "userPaused" | "steps" | "enabledWorkflowSteps" | "workflowStepResults" | "recentAgentActivityAt">,
options: TaskAgentActivityOptions = {},
): boolean {
const status = task.status;
if (
options.globalPaused === true ||
options.queued === true ||
options.isStuck === true ||
status === "queued" ||
status === "stuck-killed" ||
task.paused === true ||
task.userPaused === true ||
status === "paused" ||
status === "failed" ||
status === "awaiting-approval" ||
status === "awaiting-user-input" ||
task.column === "done" ||
task.column === "archived" ||
status === "done"
) {
return false;
}
const recentPlannerActivityAtMs = Date.parse(task.recentAgentActivityAt ?? "");
const nowMs = Date.now();
const hasFreshPlannerActivity = task.column === "triage"
&& Number.isFinite(recentPlannerActivityAtMs)
&& nowMs - recentPlannerActivityAtMs >= 0
&& nowMs - recentPlannerActivityAtMs <= RECENT_PLANNER_ACTIVITY_WINDOW_MS;
return task.column === "in-progress" ||
ACTIVE_STATUSES.has(status ?? "") ||
hasFreshPlannerActivity ||
getUnifiedTaskProgress(task).items.some((item) => item.status === "running");
}