diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index 7b912a49c7..228969db31 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -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({ {t("listView.stuck", "Stuck")} - ) : visualStatus ? ( + ) : showStatusBadge ? ( 0) || Boolean(hasInReviewStall && stallCopy) @@ -2981,7 +2989,7 @@ function TaskCardComponent({ {pausedByAgent ? t("tasks.pausedByAgent", "paused by agent") : t("tasks.paused", "paused")} )} - {!isPaused && visualStatus && visualStatus !== "queued" && ( + {showStatusBadge && ( )} {planReviewRunning && isAgentActive && ( diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx index bc8c675ff3..1eead0968c 100644 --- a/packages/dashboard/app/components/__tests__/ListView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx @@ -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(); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 769a110b99..70e4041acd 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -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( + , + ); + + 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( + , + ); + expect(screen.getByText("planning")).toBeDefined(); + + rerender( + , + ); + expect(screen.getByText("executing")).toBeDefined(); + }); + it("renders merge-remediation status as merge-active for in-review tasks", () => { const { container } = render( 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"]) { diff --git a/packages/dashboard/app/utils/taskStatusBadgeLabel.ts b/packages/dashboard/app/utils/taskStatusBadgeLabel.ts index 5ed384b16a..8547489bc9 100644 --- a/packages/dashboard/app/utils/taskStatusBadgeLabel.ts +++ b/packages/dashboard/app/utils/taskStatusBadgeLabel.ts @@ -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">,