From 69bd64d950016a31e52d6b641685d87b94f0dab8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 22 Jul 2026 00:15:54 -0700 Subject: [PATCH] FN-8475: show planning status badges in Todo Restore truthful planning status badges for Coding Ideas tasks across Todo views. - Replace column-based planning badge suppression with real-status detection - Render planning badges in board cards and desktop/mobile list rows - Add regression coverage and a patch changeset Files changed: .changeset/fn-8475-planning-status-badge.md | 7 ++++ packages/dashboard/app/components/ListView.tsx | 11 +++--- packages/dashboard/app/components/TaskCard.tsx | 7 ++-- .../app/components/__tests__/ListView.test.tsx | 40 ++++++++++++++-------- .../app/components/__tests__/TaskCard.test.tsx | 10 +++--- .../utils/__tests__/taskStatusBadgeLabel.test.ts | 33 +++++++++--------- .../dashboard/app/utils/taskStatusBadgeLabel.ts | 16 ++++----- 7 files changed, 70 insertions(+), 54 deletions(-) Fusion-Task-Id: FN-8475 Fusion-Task-Lineage: 98bff84e-bee7-4e3c-b7b2-e0a24709576b Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8475-planning-status-badge.md | 7 ++++ .../dashboard/app/components/ListView.tsx | 11 +++-- .../dashboard/app/components/TaskCard.tsx | 7 ++-- .../components/__tests__/ListView.test.tsx | 40 ++++++++++++------- .../components/__tests__/TaskCard.test.tsx | 10 +++-- .../__tests__/taskStatusBadgeLabel.test.ts | 33 +++++++-------- .../app/utils/taskStatusBadgeLabel.ts | 16 +++----- 7 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 .changeset/fn-8475-planning-status-badge.md diff --git a/.changeset/fn-8475-planning-status-badge.md b/.changeset/fn-8475-planning-status-badge.md new file mode 100644 index 0000000000..3cb4701363 --- /dev/null +++ b/.changeset/fn-8475-planning-status-badge.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show Planning status badges for active Coding Ideas Todo tasks. +category: fix +dev: Removes the column-only Todo/In-progress planning badge suppression so Board and List views use the task's real status. diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index 89f5ce2bbf..b3950da456 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 { getRunningOptionalGateBadge, getRunningWorkflowStepLabel, getUnifiedTaskProgress } from "../utils/taskProgress"; import { isTaskAgentActive } from "../utils/taskActivity"; -import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../utils/taskStatusBadgeLabel"; +import { getTaskStatusBadgeLabel, hasTaskStatusBadge } from "../utils/taskStatusBadgeLabel"; import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval"; import { useConfirm } from "../hooks/useConfirm"; import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete"; @@ -2678,9 +2678,8 @@ export function ListView({ && !visualStatus && Boolean(task.recentAgentActivityAt) && isAgentActive; - const hasStatus = (typeof visualStatus === "string" && visualStatus.trim().length > 0 - || isTransientPlannerActive) - && !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column }); + const hasStatus = (hasTaskStatusBadge(visualStatus) && visualStatus !== "queued") + || isTransientPlannerActive; const isReviewBudgetExhausted = isReviewBudgetExhaustedApproval(task); const optionalGateBadge = getRunningOptionalGateBadge(task); const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive; @@ -2916,8 +2915,8 @@ export function ListView({ && !visualStatus && Boolean(task.recentAgentActivityAt) && isAgentActive; - const showStatusBadge = (Boolean(visualStatus) || isTransientPlannerActive) - && !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column }); + const showStatusBadge = (hasTaskStatusBadge(visualStatus) && visualStatus !== "queued") + || isTransientPlannerActive; const optionalGateBadge = getRunningOptionalGateBadge(task); const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive; const isDragging = draggingTaskId === task.id; diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 0547c563c3..b13d0435c4 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -43,7 +43,7 @@ import { getRunningOptionalGateBadge, getRunningWorkflowStepLabel, getUnifiedTas import { ACTIVE_STATUSES, isTaskAgentActive } from "../utils/taskActivity"; import { getPrBadgeModifierClass } from "../utils/prBadgeClass"; import { getTotalAgentActiveMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming"; -import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../utils/taskStatusBadgeLabel"; +import { getTaskStatusBadgeLabel, hasTaskStatusBadge } from "../utils/taskStatusBadgeLabel"; import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval"; import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback"; import type { ToastType } from "../hooks/useToast"; @@ -2973,9 +2973,8 @@ function TaskCardComponent({ && Boolean(task.recentAgentActivityAt) && isAgentActive; const showStatusBadge = !isPaused - && (Boolean(visualStatus) || isTransientPlannerActive) - && visualStatus !== "queued" - && !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column }); + && (hasTaskStatusBadge(visualStatus) || isTransientPlannerActive) + && visualStatus !== "queued"; const hasCardMetaBadges = showPriorityBadge || task.executionMode === "fast" // FNXC:PlannerOversight 2026-07-04-00:00: the oversight badge is opt-in diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx index f832ffed33..7df76df9b3 100644 --- a/packages/dashboard/app/components/__tests__/ListView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx @@ -2169,16 +2169,16 @@ describe("ListView", () => { expect(screen.queryByText("Reviewing")).not.toBeInTheDocument(); }); - it("FN-8170 suppresses stale planning only on Todo and In Progress table rows", () => { + it("FN-8475 renders Todo planning in desktop table rows without a placeholder", () => { 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-8475-todo", column: "todo", status: "planning" }), + createMockTask({ id: "FN-8475-active", column: "in-progress", status: "planning" }), + createMockTask({ id: "FN-8475-triage", column: "triage", status: "planning" }), createMockTask({ - id: "FN-8170-executing", + id: "FN-8475-executing", column: "in-progress", status: "executing", steps: [{ name: "Running step", status: "in-progress" }], @@ -2186,13 +2186,26 @@ describe("ListView", () => { ], }); - for (const id of ["FN-8170-todo", "FN-8170-active"]) { + for (const id of ["FN-8475-todo", "FN-8475-active", "FN-8475-triage"]) { const row = screen.getByText(id).closest("tr") as HTMLElement; - expect(within(row).queryByText("planning")).toBeNull(); - expect(row.querySelector(".list-status-badge")).toHaveTextContent("-"); + expect(within(row).getByText("planning")).toHaveClass("list-status-badge"); + expect(row.querySelector(".list-status-badge")).not.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(); + expect(within(screen.getByText("FN-8475-executing").closest("tr") as HTMLElement).getByText("executing")).toBeInTheDocument(); + } finally { + matchMediaSpy.mockRestore(); + } + }); + + it("FN-8475 renders Todo planning in grouped mobile cards", () => { + const matchMediaSpy = mockMobileViewport(); + try { + renderListView({ + tasks: [createMockTask({ id: "FN-8475-todo-mobile", column: "todo", status: "planning" })], + }); + + const card = screen.getByText("FN-8475-todo-mobile").closest(".list-card") as HTMLElement; + expect(within(card).getByText("planning")).toHaveClass("list-status-badge"); } finally { matchMediaSpy.mockRestore(); } @@ -5170,7 +5183,7 @@ 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", () => { + it("FN-8475 renders Todo and In Progress planning in mobile cards", () => { mockMobileViewport(); const { container } = renderListView({ @@ -5187,10 +5200,9 @@ describe("ListView - Bulk Selection", () => { ], }); - for (const id of ["FN-8170-mobile-todo", "FN-8170-mobile-active"]) { - expect(within(container.querySelector(`[data-id="${id}"]`) as HTMLElement).queryByText("planning")).toBeNull(); + for (const id of ["FN-8170-mobile-todo", "FN-8170-mobile-active", "FN-8170-mobile-triage"]) { + expect(within(container.querySelector(`[data-id="${id}"]`) as HTMLElement).getByText("planning")).toHaveClass("list-status-badge"); } - 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(); }); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 301c1b3d5b..ce88fc7a80 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -2295,7 +2295,8 @@ describe("TaskCard", () => { 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 }) => { + { column: "triage" as const, status: "planning" }, + ])("FN-8475 renders real planning status and a non-empty header wrapper on $column cards", ({ column, status }) => { const { container } = render( { />, ); - expect(screen.queryByText("planning")).toBeNull(); - expect(container.querySelector(".card-header-badges")).toBeNull(); + const badge = screen.getByText("planning"); + expect(badge).toHaveClass("card-status-badge"); + expect(container.querySelector(".card-header-badges")).toContainElement(badge); }); - it("FN-8170 preserves triage planning and non-planning status badges", () => { + it("FN-8475 preserves non-planning status badges", () => { const { rerender } = render( fallback ?? key) as TFunction<"app">; -describe("shouldSuppressPlanningStatusBadge", () => { +describe("hasTaskStatusBadge", () => { 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); + "planning", + "executing", + "reviewing", + "merging", + "failed", + "needs-replan", + "done", + ])("keeps a real status visible regardless of column placement: %s", (status) => { + expect(hasTaskStatusBadge(status)).toBe(true); + }); + + it("leaves null, undefined, and empty status badge-free", () => { + expect(hasTaskStatusBadge(null)).toBe(false); + expect(hasTaskStatusBadge(undefined)).toBe(false); + expect(hasTaskStatusBadge(" ")).toBe(false); }); }); diff --git a/packages/dashboard/app/utils/taskStatusBadgeLabel.ts b/packages/dashboard/app/utils/taskStatusBadgeLabel.ts index dc77ec9f8e..bde7e8fa90 100644 --- a/packages/dashboard/app/utils/taskStatusBadgeLabel.ts +++ b/packages/dashboard/app/utils/taskStatusBadgeLabel.ts @@ -6,17 +6,13 @@ 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. +FNXC:TaskStatusBadge 2026-08-19-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 shouldSuppressPlanningStatusBadge({ - status, - column, -}: { - status?: string | null; - column: string; -}): boolean { - return status === "planning" && (column === "todo" || column === "in-progress"); +export function hasTaskStatusBadge(status: string | null | undefined): boolean { + return typeof status === "string" && status.trim().length > 0; } export function getTaskStatusBadgeLabel(