Keep planning and review lifecycle badges unambiguous across board and list views. - Classify active non-planning optional review gates for badge precedence. - Suppress stale Planning badges and empty list placeholders during Code Review. - Cover card, desktop list, mobile list, and workflow helper badge states. Files changed: packages/dashboard/app/components/ListView.tsx | 32 ++++++-- packages/dashboard/app/components/TaskCard.tsx | 24 +++++- .../app/components/__tests__/ListView.test.tsx | 56 +++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 66 +++++++++++++++ .../app/utils/__tests__/taskProgress.test.ts | 96 +++++++++++++++++++++- packages/dashboard/app/utils/taskProgress.ts | 34 ++++++-- 6 files changed, 292 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-8814 Fusion-Task-Lineage: d3a9f087-02ea-4657-aa9e-30a537458cb9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
327 lines
15 KiB
TypeScript
327 lines
15 KiB
TypeScript
import { isReviewColumnRole, type ColumnRoleFlags } from "./columnRoles";
|
|
import type { Task, WorkflowStepResult, WorkflowStepPhase, StepStatus } from "@fusion/core";
|
|
|
|
/*
|
|
FNXC:WorkflowSteps 2026-06-25-00:00:
|
|
Graph-native workflow steps (plan U3). Optional workflow step status now comes from graph-written
|
|
`task.workflowStepResults` entries keyed by node id === enabledWorkflowSteps[i]; top-level workflow nodes can also record explicit `source:"node"` progress for workflows that do not project every stage into `task.steps`. The legacy
|
|
`/api/workflow-steps` DB-row name lookup was dropped, so step names resolve from `result.workflowStepName`
|
|
with a fallback to the raw id.
|
|
|
|
Render states (design-lens): the progress model distinguishes
|
|
- `pending` (enabled, never started — no `startedAt`)
|
|
- `running` (graph node active — `pending` status with a `startedAt` and no `completedAt`)
|
|
- `done` (passed)
|
|
- `advisory_failure` (non-blocking REVISE — amber, counts as completed; does not block merge)
|
|
- `failed` (blocking gate failure — red)
|
|
- `skipped`
|
|
Disabled optional steps are simply absent from `enabledWorkflowSteps`, so they never appear in the
|
|
counter/bar. Recorded workflow-node progress is included independently because it represents an actual graph stage that ran, not a toggle placeholder.
|
|
*/
|
|
|
|
export type UnifiedTaskProgressStatus = StepStatus | "failed" | "advisory_failure" | "running";
|
|
|
|
export interface UnifiedTaskProgressItem {
|
|
id: string;
|
|
name: string;
|
|
status: UnifiedTaskProgressStatus;
|
|
source: "step" | "workflow";
|
|
phase: WorkflowStepPhase;
|
|
}
|
|
|
|
export interface UnifiedTaskProgress {
|
|
total: number;
|
|
completed: number;
|
|
items: UnifiedTaskProgressItem[];
|
|
}
|
|
|
|
function mapWorkflowStatus(result: WorkflowStepResult): UnifiedTaskProgressStatus {
|
|
switch (result.status) {
|
|
case "passed":
|
|
return "done";
|
|
case "failed":
|
|
return "failed";
|
|
case "advisory_failure":
|
|
return "advisory_failure";
|
|
case "skipped":
|
|
return "skipped";
|
|
case "pending":
|
|
default:
|
|
// The graph upserts a `pending` entry when a step starts running. A started-but-not-completed
|
|
// entry is the in-progress/`running` display state; a bare `pending` (no `startedAt`) is an
|
|
// enabled step that has not begun yet.
|
|
return result.startedAt && !result.completedAt ? "running" : "pending";
|
|
}
|
|
}
|
|
|
|
function isCompleted(status: UnifiedTaskProgressStatus): boolean {
|
|
// advisory_failure is non-blocking: the step ran and returned feedback, so it counts as completed
|
|
// (overall progress reads complete when only advisory steps returned REVISE).
|
|
return status === "done" || status === "skipped" || status === "advisory_failure";
|
|
}
|
|
|
|
/*
|
|
FNXC:WorkflowStepResults 2026-06-26-16:30:
|
|
An enabled-but-not-yet-run workflow step has no recorded result yet, so there is no
|
|
`workflowStepName` to show. Rather than render the raw graph node id (e.g. `code-review`,
|
|
`browser-verification`), humanize it into a Title Case label ("Code Review",
|
|
"Browser Verification"). Once the graph records the step it carries the workflow's exact
|
|
`config.name`, which always wins; humanization is only the pre-run fallback. The UI must
|
|
show proper casing for workflow steps (e.g. "Code Review"), never the lowercase hyphenated id.
|
|
*/
|
|
function humanizeWorkflowStepId(workflowStepId: string): string {
|
|
const words = workflowStepId
|
|
.replace(/^plugin:/, "")
|
|
.split(/[-_:\s]+/)
|
|
.filter(Boolean);
|
|
if (words.length === 0) return workflowStepId;
|
|
return words
|
|
.map((w) => (/^(ux|ui|qa|ai|api|pr|id)$/i.test(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
.join(" ");
|
|
}
|
|
|
|
function resolveWorkflowStepName(workflowStepId: string, result: WorkflowStepResult | undefined): string {
|
|
const resultName = result?.workflowStepName?.trim();
|
|
if (resultName) {
|
|
return resultName;
|
|
}
|
|
return humanizeWorkflowStepId(workflowStepId);
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskCardWorkflowProgress 2026-07-21-22:26:
|
|
In-progress progress is WIP implementation only. Built-in Plan Review lives in Todo/planning, Code Review / Browser Verification / post-merge / completion-summary / merge nodes live in In-review (or post-merge), so they must not inflate the in-progress checklist as pending/done segments. Board and list progress for WIP use `scope: "implementation"`; detail and badges keep the full pipeline via the default `scope: "full"`.
|
|
*/
|
|
const NON_IMPLEMENTATION_WORKFLOW_STEP_IDS = new Set([
|
|
"plan-review",
|
|
"plan-replan",
|
|
"code-review",
|
|
"browser-verification",
|
|
"post-merge-verification",
|
|
"completion-summary",
|
|
]);
|
|
|
|
function isNonImplementationWorkflowStepId(workflowStepId: string): boolean {
|
|
return NON_IMPLEMENTATION_WORKFLOW_STEP_IDS.has(workflowStepId) || workflowStepId.startsWith("merge-");
|
|
}
|
|
|
|
export type UnifiedTaskProgressScope = "full" | "implementation";
|
|
|
|
export interface GetUnifiedTaskProgressOptions {
|
|
/**
|
|
* `full` — entire pipeline (implementation steps + optional gates + recorded nodes).
|
|
* `implementation` — WIP-only: parsed task steps plus non-lane-gate workflow/node items.
|
|
*/
|
|
scope?: UnifiedTaskProgressScope;
|
|
}
|
|
|
|
export function getUnifiedTaskProgress(
|
|
task: Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">,
|
|
options: GetUnifiedTaskProgressOptions = {},
|
|
): UnifiedTaskProgress {
|
|
const scope = options.scope ?? "full";
|
|
const stepItems: UnifiedTaskProgressItem[] = (task.steps ?? []).map((step, index) => ({
|
|
id: `step-${index}`,
|
|
name: step.name,
|
|
status: step.status,
|
|
source: "step",
|
|
phase: "pre-merge",
|
|
}));
|
|
|
|
const workflowResultsById = new Map(
|
|
(task.workflowStepResults ?? []).map((result) => [result.workflowStepId, result] as const),
|
|
);
|
|
|
|
const workflowItems: UnifiedTaskProgressItem[] = (task.enabledWorkflowSteps ?? [])
|
|
.filter((workflowStepId) => scope === "full" || !isNonImplementationWorkflowStepId(workflowStepId))
|
|
.map((workflowStepId) => {
|
|
const result = workflowResultsById.get(workflowStepId);
|
|
return {
|
|
id: `workflow-${workflowStepId}`,
|
|
name: resolveWorkflowStepName(workflowStepId, result),
|
|
status: result ? mapWorkflowStatus(result) : "pending",
|
|
source: "workflow" as const,
|
|
phase: result?.phase ?? "pre-merge",
|
|
};
|
|
});
|
|
const enabledWorkflowStepIds = new Set(task.enabledWorkflowSteps ?? []);
|
|
/*
|
|
FNXC:TaskCardWorkflowProgress 2026-06-29-15:05:
|
|
Compound Engineering runs top-level skill nodes (Plan, Execute, Commit/PR, Resolve feedback) that do real work but are not optional toggles and do not update `task.steps`. Include recorded `source:"node"` results even when `enabledWorkflowSteps` is empty so task cards and detail progress match the graph's actual active stage, while stale disabled optional-group results remain hidden.
|
|
*/
|
|
const recordedNodeItems: UnifiedTaskProgressItem[] = (task.workflowStepResults ?? [])
|
|
.filter((result) => result.source === "node" && !enabledWorkflowStepIds.has(result.workflowStepId))
|
|
.filter((result) => scope === "full" || !isNonImplementationWorkflowStepId(result.workflowStepId))
|
|
.map((result) => ({
|
|
id: `workflow-${result.workflowStepId}`,
|
|
name: resolveWorkflowStepName(result.workflowStepId, result),
|
|
status: mapWorkflowStatus(result),
|
|
source: "workflow",
|
|
phase: result.phase ?? "pre-merge",
|
|
}));
|
|
|
|
/*
|
|
FNXC:TaskCardWorkflowProgress 2026-06-29-00:41:
|
|
Plan Review is a pre-execution optional step in the default stepwise Coding workflow, so task cards must show it before parsed implementation steps. End-of-work optional steps such as Code Review stay after implementation steps so the card order matches workflow execution order.
|
|
|
|
FNXC:TaskCardWorkflowProgress 2026-07-21-22:26:
|
|
Implementation scope omits Plan Review / Code Review and other lane-owned gates, so the pre-execution reorder is only meaningful for the full pipeline view.
|
|
*/
|
|
const preExecutionWorkflowItems = scope === "full"
|
|
? workflowItems.filter((item) => item.id === "workflow-plan-review")
|
|
: [];
|
|
const remainingWorkflowItems = scope === "full"
|
|
? workflowItems.filter((item) => item.id !== "workflow-plan-review")
|
|
: workflowItems;
|
|
const items = [...preExecutionWorkflowItems, ...stepItems, ...remainingWorkflowItems, ...recordedNodeItems];
|
|
const total = items.length;
|
|
const completed = items.filter((item) => isCompleted(item.status)).length;
|
|
|
|
return { total, completed, items };
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-19-2b:55 (U12 / R2 / R11):
|
|
The workflow-step-derived badge label. Operator surfaces used to render raw engine status tokens
|
|
("planning", "needs-replan"), which name ENGINE bookkeeping rather than the stage the card is
|
|
actually in — and which no user-authored workflow has any reason to recognize. When a workflow step
|
|
is running, its own IR-declared name ("Plan Review", "Code Review") is both truer and workflow-owned,
|
|
so it takes precedence over the status vocabulary.
|
|
|
|
Returns undefined when nothing is running, leaving the status mapping as the fallback. The engine
|
|
statuses themselves are unchanged — `needs-replan` remains the graph's durable replan signal; this
|
|
only decides what the operator READS.
|
|
*/
|
|
function getRunningWorkflowProgressItem(
|
|
task: Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">,
|
|
): UnifiedTaskProgressItem | undefined {
|
|
/*
|
|
FNXC:TaskCardBadgePrecedence 2026-08-06-14:53:
|
|
A malformed snapshot can transiently contain multiple started workflow results. Progress is already
|
|
arranged in lifecycle order (Plan Review before implementation and review gates after it), so choose
|
|
the latest running workflow item rather than its transport-array position. This makes an active Code
|
|
Review authoritative over stale Planning while preserving Plan Review as the nested planning gate.
|
|
*/
|
|
return getUnifiedTaskProgress(task).items
|
|
.filter((item) => item.source === "workflow" && item.status === "running")
|
|
.at(-1);
|
|
}
|
|
|
|
export function getRunningWorkflowStepLabel(
|
|
task: Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">,
|
|
): string | undefined {
|
|
return getRunningWorkflowProgressItem(task)?.name;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskCardOptionalGateBadge 2026-07-21-22:30:
|
|
Lane-owned optional gates are header badges, not progress bullet-list rows. Code Review and Browser Verification (and post-merge verification) badge on in-review. Each badge reuses the same startedAt-without-completedAt "running" semantics as the progress list.
|
|
|
|
FNXC:TaskCardOptionalGateBadge 2026-07-27-06:10:
|
|
Plan Review's planning-lane restriction is GONE (see getRunningOptionalGateBadge) — it badges wherever it runs. The badge is keyed on the RUNNING gate rather than the card's column, so it stays correct across every workflow regardless of where that workflow places the node.
|
|
*/
|
|
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-31-15:50:
|
|
DELIBERATE-LITERAL — the unresolved-flags default, reviewed 2026-07-31-15:50.
|
|
|
|
Census-invisible: a `Set` literal is a definition, not a comparison, so nothing in the lifecycle
|
|
backlog pointed at this gate — in a file whose Plan Review badge directly above was already converted
|
|
off its column restriction. One converted badge and one unconverted one, deciding sibling questions.
|
|
|
|
Keyed on the literal, the code-review / browser-verification / post-merge badges never appeared on a
|
|
renamed board: the gate WAS running and the card simply showed nothing, which reads as an idle card
|
|
rather than a missing badge. Cosmetic in consequence, but it is the surface an operator watches to
|
|
know a review is in flight.
|
|
*/
|
|
const REVIEW_LANE_COLUMNS = new Set(["in-review"]);
|
|
|
|
export interface RunningOptionalGateBadge {
|
|
workflowStepId: string;
|
|
/** Full step name for titles / a11y. */
|
|
name: string;
|
|
/** Compact card/list label. */
|
|
label: string;
|
|
/** Stable test id fragment after `card-` / `list-`. */
|
|
testId: string;
|
|
}
|
|
|
|
function workflowStepIdFromProgressItemId(itemId: string): string {
|
|
return itemId.startsWith("workflow-") ? itemId.slice("workflow-".length) : itemId;
|
|
}
|
|
|
|
export function getRunningOptionalGateBadge(
|
|
task: Pick<Task, "column" | "steps" | "enabledWorkflowSteps" | "workflowStepResults">,
|
|
/** Resolved trait flags for the card's column; omitted keeps the legacy id. */
|
|
columnFlags?: ColumnRoleFlags,
|
|
): RunningOptionalGateBadge | undefined {
|
|
const running = getRunningWorkflowProgressItem(task);
|
|
if (!running) return undefined;
|
|
|
|
const workflowStepId = workflowStepIdFromProgressItemId(running.id);
|
|
if (!isNonImplementationWorkflowStepId(workflowStepId)) return undefined;
|
|
|
|
if (workflowStepId === "plan-review" || workflowStepId === "plan-replan") {
|
|
/*
|
|
FNXC:TaskCardOptionalGateBadge 2026-07-27-06:10:
|
|
Plan Review badges wherever it RUNS. A workflow places this node where its own lane shape calls
|
|
for it — the built-in coding workflows run it in the planning column, a custom workflow may not —
|
|
and a lane gate here silently suppressed the badge for every placement it did not anticipate. The
|
|
gate's own running state is the signal; the card's column is not a second opinion on it. Code
|
|
Review / Browser Verification keep their in-review lane gate below, because those genuinely DO
|
|
move the card into the review column when they run.
|
|
*/
|
|
return {
|
|
workflowStepId,
|
|
name: running.name,
|
|
/*
|
|
FNXC:TaskCardOptionalGateBadge 2026-07-26-14:05:
|
|
The badge names the GATE, not a generic activity: the short "Reviewing" copy (FN-7831) read
|
|
identically to a code-review badge and did not tell the operator which review was running.
|
|
Plan Review and its replan loop both badge as "Plan Review". The `reviewing` testId is
|
|
unchanged so existing board/list selectors keep working.
|
|
*/
|
|
label: "Plan Review",
|
|
testId: "reviewing",
|
|
};
|
|
}
|
|
|
|
if (!(columnFlags ? isReviewColumnRole(columnFlags, task.column) : REVIEW_LANE_COLUMNS.has(task.column))) return undefined;
|
|
if (
|
|
workflowStepId !== "code-review"
|
|
&& workflowStepId !== "browser-verification"
|
|
&& workflowStepId !== "post-merge-verification"
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
workflowStepId,
|
|
name: running.name,
|
|
label: running.name,
|
|
testId: workflowStepId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether a rendered optional-gate badge displaces the general Planning status badge.
|
|
* Plan Review and its replan loop are deliberately excluded because they describe nested planning.
|
|
*/
|
|
export function isNonPlanningOptionalGateBadge(
|
|
badge: RunningOptionalGateBadge | undefined,
|
|
): boolean {
|
|
return Boolean(badge && badge.workflowStepId !== "plan-review" && badge.workflowStepId !== "plan-replan");
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:00:
|
|
FN-7831 requires task cards and list rows to show a distinct "Reviewing" badge only while the optional `plan-review` workflow step is actively running. Reuse the unified progress item status so every board surface follows the same startedAt-without-completedAt semantics as the progress list.
|
|
|
|
FNXC:TaskCardOptionalGateBadge 2026-07-21-22:30:
|
|
Kept as a thin plan-review predicate for Ready-badge suppression and older call sites; header rendering prefers getRunningOptionalGateBadge for all lane-owned gates.
|
|
*/
|
|
export function isPlanReviewRunning(task: Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">): boolean {
|
|
return getUnifiedTaskProgress(task).items.some(
|
|
(item) => item.id === "workflow-plan-review" && item.status === "running",
|
|
);
|
|
}
|