Restore visible lifecycle badges for empty-status tasks in WIP lanes. - Derive fallback badge labels from resolved workflow WIP traits and lane names - Render the fallback consistently on board cards and desktop/mobile list rows - Cover empty, custom-lane, populated, and paused status precedence Files changed: .changeset/fn-8826-dashboard-wip-badge.md | 7 +++ packages/dashboard/app/components/ListView.tsx | 37 +++++++++++-- packages/dashboard/app/components/TaskCard.tsx | 19 +++++-- .../app/components/__tests__/ListView.test.tsx | 61 ++++++++++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 38 +++++++++++++- .../dashboard/app/utils/taskStatusBadgeLabel.ts | 24 +++++++++ 6 files changed, 177 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-8826 Fusion-Task-Lineage: fd9c857a-a25f-44d0-ae66-502bc1bd860e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
157 lines
7.5 KiB
TypeScript
157 lines
7.5 KiB
TypeScript
/*
|
|
FNXC:MergeQueue 2026-07-15-10:45:
|
|
AI merge sets task.status to reviewing/landing for most of the live merge window. Board/list badges must never show those raw engine strings; map the full active-merge pipeline to operator-facing Merging… (and Merging fixes… for merging-fix).
|
|
*/
|
|
import type { TFunction } from "i18next";
|
|
import type { Task } from "@fusion/core";
|
|
import { isActiveMergeStatus } from "../../../core/src/merge/active-merge-status";
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-22-00:00:
|
|
FN-8475 restores truthful status visibility: Coding (Ideas) deliberately plans in Todo,
|
|
so a real non-queued task status must not be hidden based only on its board column.
|
|
Queued remains an intake-only presentation exclusion at TaskCard and ListView call sites.
|
|
*/
|
|
export function hasTaskStatusBadge(status: string | null | undefined): boolean {
|
|
return typeof status === "string" && status.trim().length > 0;
|
|
}
|
|
|
|
export interface TaskWipLifecycleBadgeContext {
|
|
/** True when the task's resolved workflow column occupies an implementation/WIP slot. */
|
|
isWipColumn: boolean;
|
|
/** The resolved workflow column name, when board metadata has loaded. */
|
|
lifecycleLabel?: string | null;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-08-08-03:02:
|
|
A WIP lifecycle role supplies a badge only when the persisted status is empty: engine status,
|
|
pause, workflow-gate, and other call-site precedence remains authoritative. Resolve WIP from
|
|
workflow traits before calling this helper so renamed/custom implementation lanes receive their
|
|
own lifecycle label; metadata gaps retain the safe legacy "In progress" fallback without writing
|
|
that presentation state back to task.status.
|
|
*/
|
|
export function getTaskWipLifecycleBadgeLabel(
|
|
status: string | null | undefined,
|
|
t: TFunction<"app">,
|
|
{ isWipColumn, lifecycleLabel }: TaskWipLifecycleBadgeContext,
|
|
): string | null {
|
|
if (!isWipColumn || hasTaskStatusBadge(status)) return null;
|
|
return lifecycleLabel?.trim() || t("tasks.statusInProgress", "In progress");
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskStatusConsistency 2026-08-05-03:49:
|
|
A planner agent-log event can reach the shared board snapshot before the status update that changes
|
|
`needs-replan` to `planning`. Its `recentAgentActivityAt` is written only after the log proves it is
|
|
newer than the row, so this is explicit live-planner evidence rather than arrival-order guessing.
|
|
Use it only for the parked-revision token while the event is inside the five-minute live window and
|
|
the engine is not globally paused: undefined status remains unknown and an idle needs-replan card
|
|
remains Queued to revise. TaskCard, both ListView paths, and task detail share this predicate so a
|
|
modal that has already fetched planning cannot contradict the board during that short SSE gap.
|
|
|
|
FNXC:TaskStatusConsistency 2026-08-05-04:54:
|
|
A parseable historical log is not liveness. The transient `needs-replan` override expires after the
|
|
shared five-minute heartbeat window and is suppressed during global pause, returning the operator to
|
|
Queued to revise instead of leaving every affected surface stuck on Planning.
|
|
*/
|
|
export const PLANNER_ACTIVITY_LIVE_WINDOW_MS = 5 * 60_000;
|
|
|
|
export interface TaskPlanningActivityOptions {
|
|
globalPaused?: boolean;
|
|
now?: number;
|
|
}
|
|
|
|
export function isTaskPlanningActive(
|
|
task: Pick<Task, "status" | "recentAgentActivityAt">,
|
|
{ globalPaused = false, now = Date.now() }: TaskPlanningActivityOptions = {},
|
|
): boolean {
|
|
if (task.status === "planning") return true;
|
|
if (globalPaused || task.status !== "needs-replan" || typeof task.recentAgentActivityAt !== "string") return false;
|
|
|
|
const activityAt = Date.parse(task.recentAgentActivityAt);
|
|
return Number.isFinite(activityAt)
|
|
&& activityAt <= now
|
|
&& now - activityAt <= PLANNER_ACTIVITY_LIVE_WINDOW_MS;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-08-01-01:30 (operator: "make the badges more descriptive"):
|
|
Three waiting states read as ACTIVITY on the board and made healthy queueing look broken:
|
|
"Revising" while merely queued for a replan slot, and bare "queued" while parked behind another
|
|
task's file-scope lease (the reason lived only in a log line). The badge now names the wait:
|
|
"Queued to revise" (idle needs-replan) and "Queued behind FN-XXXX" (overlap lease). Liveness and
|
|
the overlap id are caller-supplied so this stays a pure mapper; omitted → legacy labels, so every
|
|
existing caller keeps its behavior.
|
|
*/
|
|
export interface TaskStatusBadgeContext {
|
|
/** True when no agent session is live for the card (queued, not running). */
|
|
idle?: boolean;
|
|
/** The task currently holding the overlapping file-scope lease, when queued behind one. */
|
|
overlapBlockedBy?: string | null;
|
|
}
|
|
|
|
export function getTaskStatusBadgeLabel(
|
|
status: string | null | undefined,
|
|
t: TFunction<"app">,
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-19-02:55 (U12 / R2 / R11):
|
|
Workflow-step state wins over the raw status vocabulary. A card whose Plan Review is running
|
|
reads "Plan Review" — the step's own IR-declared name — instead of the engine token "planning"
|
|
or "needs-replan". Pass `getRunningWorkflowStepLabel(task)` here; omit it and the legacy status
|
|
mapping below is unchanged, so every existing caller keeps its behavior.
|
|
*/
|
|
workflowStepLabel?: string,
|
|
context?: TaskStatusBadgeContext,
|
|
): string {
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-19-09:40:
|
|
Every active-merge status ("merging", "merging-pr", "merging-fix", "reviewing", "landing") must
|
|
win over a still-running workflow-step label (a pre-merge step's startedAt-without-completedAt
|
|
state can survive into the merge pipeline). Checking the status before the workflow-step override
|
|
enforces this for every caller (TaskCard, ListView grouped rows, ListView table rows) instead of
|
|
relying on per-call-site pre-checks. "merging-fix" keeps its distinct "Merging fixes…" label.
|
|
*/
|
|
if (isActiveMergeStatus(status)) {
|
|
return status === "merging-fix"
|
|
? t("tasks.statusMergingFix", "Merging fixes…")
|
|
: t("tasks.statusMerging", "Merging…");
|
|
}
|
|
if (workflowStepLabel) return workflowStepLabel;
|
|
if (!status) return "";
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-22-09:22:
|
|
FN-8493 requires operator copy "Revising" while a revise/replan cycle is in progress. Keep the
|
|
engine token "needs-replan" unchanged and map centrally so board cards and list rows agree.
|
|
*/
|
|
if (status === "needs-replan") {
|
|
// Idle = waiting for a planning slot; the live revise cycle shows as "Revising"/step label.
|
|
return context?.idle
|
|
? t("tasks.statusReplanQueued", "Queued to revise")
|
|
: t("tasks.statusReplan", "Revising");
|
|
}
|
|
if (status === "queued" && context?.overlapBlockedBy) {
|
|
return t("tasks.statusQueuedBehind", "Queued behind {{taskId}}", { taskId: context.overlapBlockedBy });
|
|
}
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-08-01-03:20 (operator: one queued family, like the planning badge):
|
|
Raw "queued" (scheduler capacity/dependency marker) previously fell through to the verbatim
|
|
lowercase engine token. Map it so every queued state reads as the same badge family.
|
|
*/
|
|
if (status === "queued") {
|
|
return t("tasks.statusQueued", "Queued");
|
|
}
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-26-14:05:
|
|
"planning" is an engine token, not operator copy, and it was only ever hidden because U12's
|
|
workflow-step override happened to cover the same cards. Now that the Plan Review gate has its own
|
|
badge, callers suppress the step-name override while that badge renders — which uncovered the raw
|
|
lowercase token underneath. Map it to the same "Planning" copy the transient-planner badge already
|
|
uses so the two paths cannot read differently.
|
|
*/
|
|
if (status === "planning") {
|
|
return t("tasks.statusPlanning", "Planning");
|
|
}
|
|
return status;
|
|
}
|