FN-8170: suppress stale planning status badges
Hide stale planning badges from Todo and In Progress task surfaces. - Centralize the planning badge suppression predicate. - Apply it to board cards and desktop/mobile list views. - Cover suppressed and preserved status badge behavior. Files changed: packages/dashboard/app/components/ListView.tsx | 11 +++-- packages/dashboard/app/components/TaskCard.tsx | 16 +++++-- .../app/components/__tests__/ListView.test.tsx | 53 ++++++++++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 40 ++++++++++++++++ .../utils/__tests__/taskStatusBadgeLabel.test.ts | 21 ++++++++- .../dashboard/app/utils/taskStatusBadgeLabel.ts | 14 ++++++ 6 files changed, 147 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-8170 Fusion-Task-Lineage: e2a00154-8537-4eb3-aadb-45b42f13f15b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -24,7 +24,7 @@ import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/project
|
||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection";
|
||||
import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgress";
|
||||
import { isTaskAgentActive } from "../utils/taskActivity";
|
||||
import { getTaskStatusBadgeLabel } from "../utils/taskStatusBadgeLabel";
|
||||
import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../utils/taskStatusBadgeLabel";
|
||||
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
||||
import { useConfirm } from "../hooks/useConfirm";
|
||||
import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete";
|
||||
@@ -2669,7 +2669,10 @@ export function ListView({
|
||||
const isPaused = !isDoneColumn && task.paused === true;
|
||||
const isStuckState = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
|
||||
const isAgentActive = isTaskAgentActive(task, { globalPaused, isStuck: isStuckState });
|
||||
const hasStatus = typeof visualStatus === "string" && visualStatus.trim().length > 0;
|
||||
// FNXC:TaskStatusBadge 2026-07-16-12:00: FN-8170 keeps mobile and table list status rendering aligned with TaskCard through the shared Todo/In Progress planning suppression predicate.
|
||||
const hasStatus = typeof visualStatus === "string"
|
||||
&& visualStatus.trim().length > 0
|
||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
||||
const isReviewBudgetExhausted = isReviewBudgetExhaustedApproval(task);
|
||||
const planReviewRunning = isPlanReviewRunning(task);
|
||||
const hasDependencies = Boolean(task.dependencies && task.dependencies.length > 0);
|
||||
@@ -2883,6 +2886,8 @@ export function ListView({
|
||||
const isStuckState = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
|
||||
const isAgentActive = isTaskAgentActive(task, { globalPaused, isStuck: isStuckState });
|
||||
const isReviewBudgetExhausted = isReviewBudgetExhaustedApproval(task);
|
||||
const showStatusBadge = Boolean(visualStatus)
|
||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
||||
const planReviewRunning = isPlanReviewRunning(task);
|
||||
const isDragging = draggingTaskId === task.id;
|
||||
|
||||
@@ -2947,7 +2952,7 @@ export function ListView({
|
||||
<span className="list-status-badge stuck">
|
||||
{t("listView.stuck", "Stuck")}
|
||||
</span>
|
||||
) : visualStatus ? (
|
||||
) : showStatusBadge ? (
|
||||
<span
|
||||
className={`list-status-badge list-status-badge--${task.column}${isReviewBudgetExhausted ? " list-status-badge--review-budget-exhausted" : ""}${isFailed ? " failed" : ""}${
|
||||
isAgentActive ? " pulsing" : ""
|
||||
|
||||
@@ -43,7 +43,7 @@ import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgre
|
||||
import { ACTIVE_STATUSES, isTaskAgentActive } from "../utils/taskActivity";
|
||||
import { getPrBadgeModifierClass } from "../utils/prBadgeClass";
|
||||
import { getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming";
|
||||
import { getTaskStatusBadgeLabel } from "../utils/taskStatusBadgeLabel";
|
||||
import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../utils/taskStatusBadgeLabel";
|
||||
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
||||
import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -2857,6 +2857,14 @@ function TaskCardComponent({
|
||||
* guard — operators found it fired as noise on nearly every in-progress
|
||||
* card. The oversight-level badge (`showOversightBadge`) is untouched.
|
||||
*/
|
||||
/*
|
||||
FNXC:TaskStatusBadge 2026-07-16-12:00:
|
||||
FN-8170 shares this predicate with ListView so stale planning status never produces a Todo/In Progress board badge or its otherwise-empty header wrapper.
|
||||
*/
|
||||
const showStatusBadge = !isPaused
|
||||
&& Boolean(visualStatus)
|
||||
&& visualStatus !== "queued"
|
||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
||||
const hasCardMetaBadges = showPriorityBadge
|
||||
|| task.executionMode === "fast"
|
||||
// FNXC:PlannerOversight 2026-07-04-00:00: the oversight badge is opt-in
|
||||
@@ -2864,7 +2872,7 @@ function TaskCardComponent({
|
||||
// guard so `.card-meta-badges` only renders when it has a real child.
|
||||
|| showOversightBadge;
|
||||
const hasHeaderBadges = Boolean(isPaused)
|
||||
|| Boolean(!isPaused && visualStatus && visualStatus !== "queued")
|
||||
|| showStatusBadge
|
||||
|| (planReviewRunning && isAgentActive)
|
||||
|| Boolean(!isPaused && task.column === "todo" && !visualStatus && (task.steps?.length ?? 0) > 0)
|
||||
|| Boolean(hasInReviewStall && stallCopy)
|
||||
@@ -2981,7 +2989,7 @@ function TaskCardComponent({
|
||||
{pausedByAgent ? t("tasks.pausedByAgent", "paused by agent") : t("tasks.paused", "paused")}
|
||||
</span>
|
||||
)}
|
||||
{!isPaused && visualStatus && visualStatus !== "queued" && (
|
||||
{showStatusBadge && (
|
||||
<span
|
||||
className={`card-status-badge card-status-badge--${task.column}${isAwaitingApproval ? " awaiting-approval" : ""}${isPlanReviewReplanCapApproval ? " awaiting-approval--plan-review-replan-cap" : ""}${isAwaitingInput ? " awaiting-input" : ""}${isAgentActive ? " pulsing" : ""}${isFailed ? " failed" : ""}${isStuck ? " stuck" : ""}`}
|
||||
title={
|
||||
@@ -3010,7 +3018,7 @@ function TaskCardComponent({
|
||||
? t("tasks.needsInput", "Needs input")
|
||||
: visualStatus === "merging-fix"
|
||||
? t("tasks.statusMergingFix", "Merging fixes…")
|
||||
: getTaskStatusLabel(visualStatus, t)}
|
||||
: getTaskStatusLabel(visualStatus!, t)}
|
||||
</span>
|
||||
)}
|
||||
{planReviewRunning && isAgentActive && (
|
||||
|
||||
@@ -2131,6 +2131,35 @@ describe("ListView", () => {
|
||||
expect(screen.queryByText("Reviewing")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("FN-8170 suppresses stale planning only on Todo and In Progress table rows", () => {
|
||||
const matchMediaSpy = mockDesktopViewport();
|
||||
try {
|
||||
renderListView({
|
||||
tasks: [
|
||||
createMockTask({ id: "FN-8170-todo", column: "todo", status: "planning" }),
|
||||
createMockTask({ id: "FN-8170-active", column: "in-progress", status: "planning" }),
|
||||
createMockTask({ id: "FN-8170-triage", column: "triage", status: "planning" }),
|
||||
createMockTask({
|
||||
id: "FN-8170-executing",
|
||||
column: "in-progress",
|
||||
status: "executing",
|
||||
steps: [{ name: "Running step", status: "in-progress" }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
for (const id of ["FN-8170-todo", "FN-8170-active"]) {
|
||||
const row = screen.getByText(id).closest("tr") as HTMLElement;
|
||||
expect(within(row).queryByText("planning")).toBeNull();
|
||||
expect(row.querySelector(".list-status-badge")).toHaveTextContent("-");
|
||||
}
|
||||
expect(within(screen.getByText("FN-8170-triage").closest("tr") as HTMLElement).getByText("planning")).toBeInTheDocument();
|
||||
expect(within(screen.getByText("FN-8170-executing").closest("tr") as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||
} finally {
|
||||
matchMediaSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders paused tasks with dimmed styling", () => {
|
||||
const tasks = [createMockTask({ id: "FN-001", paused: true })];
|
||||
|
||||
@@ -5103,6 +5132,30 @@ describe("ListView - Bulk Selection", () => {
|
||||
expect(within(card as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("FN-8170 suppresses stale planning only on Todo and In Progress mobile cards", () => {
|
||||
mockMobileViewport();
|
||||
|
||||
const { container } = renderListView({
|
||||
tasks: [
|
||||
createMockTask({ id: "FN-8170-mobile-todo", column: "todo", status: "planning" }),
|
||||
createMockTask({ id: "FN-8170-mobile-active", column: "in-progress", status: "planning" }),
|
||||
createMockTask({ id: "FN-8170-mobile-triage", column: "triage", status: "planning" }),
|
||||
createMockTask({
|
||||
id: "FN-8170-mobile-executing",
|
||||
column: "in-progress",
|
||||
status: "executing",
|
||||
steps: [{ name: "Running step", status: "in-progress" }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
for (const id of ["FN-8170-mobile-todo", "FN-8170-mobile-active"]) {
|
||||
expect(within(container.querySelector(`[data-id="${id}"]`) as HTMLElement).queryByText("planning")).toBeNull();
|
||||
}
|
||||
expect(within(container.querySelector('[data-id="FN-8170-mobile-triage"]') as HTMLElement).getByText("planning")).toBeInTheDocument();
|
||||
expect(within(container.querySelector('[data-id="FN-8170-mobile-executing"]') as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows fast indicator in mobile cards only for fast-mode tasks", () => {
|
||||
mockMobileViewport();
|
||||
|
||||
|
||||
@@ -2176,6 +2176,46 @@ describe("TaskCard", () => {
|
||||
expect(screen.getByText("executing")).toBeDefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ column: "todo" as const, status: "planning" },
|
||||
{ column: "in-progress" as const, status: "planning" },
|
||||
])("FN-8170 suppresses stale planning status and its empty header wrapper on $column cards", ({ column, status }) => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column, status })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("planning")).toBeNull();
|
||||
expect(container.querySelector(".card-header-badges")).toBeNull();
|
||||
});
|
||||
|
||||
it("FN-8170 preserves triage planning and non-planning status badges", () => {
|
||||
const { rerender } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "triage", status: "planning" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("planning")).toBeDefined();
|
||||
|
||||
rerender(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
column: "in-progress",
|
||||
status: "executing",
|
||||
steps: [{ name: "Executing step", status: "in-progress" }],
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("executing")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders merge-remediation status as merge-active for in-review tasks", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TFunction } from "i18next";
|
||||
import { getTaskStatusBadgeLabel } from "../taskStatusBadgeLabel";
|
||||
import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../taskStatusBadgeLabel";
|
||||
|
||||
const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction<"app">;
|
||||
|
||||
describe("shouldSuppressPlanningStatusBadge", () => {
|
||||
it.each([
|
||||
{ status: "planning", column: "todo", suppressed: true },
|
||||
{ status: "planning", column: "in-progress", suppressed: true },
|
||||
{ status: "planning", column: "triage", suppressed: false },
|
||||
{ status: "executing", column: "todo", suppressed: false },
|
||||
{ status: "executing", column: "in-progress", suppressed: false },
|
||||
{ status: "reviewing", column: "todo", suppressed: false },
|
||||
{ status: "merging", column: "in-progress", suppressed: false },
|
||||
{ status: "failed", column: "todo", suppressed: false },
|
||||
{ status: "needs-replan", column: "in-progress", suppressed: false },
|
||||
{ status: "done", column: "todo", suppressed: false },
|
||||
{ status: null, column: "in-progress", suppressed: false },
|
||||
{ status: undefined, column: "todo", suppressed: false },
|
||||
])("suppresses only stale planning status for Todo and In Progress: $status/$column", ({ status, column, suppressed }) => {
|
||||
expect(shouldSuppressPlanningStatusBadge({ status, column })).toBe(suppressed);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTaskStatusBadgeLabel", () => {
|
||||
it("maps the full AI merge pipeline to Merging…", () => {
|
||||
for (const status of ["merging", "merging-pr", "reviewing", "landing"]) {
|
||||
|
||||
@@ -5,6 +5,20 @@ AI merge sets task.status to reviewing/landing for most of the live merge window
|
||||
import type { TFunction } from "i18next";
|
||||
import { isActiveMergeStatus } from "../../../core/src/active-merge-status";
|
||||
|
||||
/*
|
||||
FNXC:TaskStatusBadge 2026-07-16-12:00:
|
||||
FN-8170 requires the raw "planning" status badge to stay off Todo and In Progress cards while preserving it in triage. Suppression is unconditional because Coding (Ideas) in-place planning writes only status:"planning", with no durable client-visible active-planner signal; the additive Reviewing Plan Review badge remains independent.
|
||||
*/
|
||||
export function shouldSuppressPlanningStatusBadge({
|
||||
status,
|
||||
column,
|
||||
}: {
|
||||
status?: string | null;
|
||||
column: string;
|
||||
}): boolean {
|
||||
return status === "planning" && (column === "todo" || column === "in-progress");
|
||||
}
|
||||
|
||||
export function getTaskStatusBadgeLabel(
|
||||
status: string | null | undefined,
|
||||
t: TFunction<"app">,
|
||||
|
||||
Reference in New Issue
Block a user