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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8475-planning-status-badge.md
Normal file
7
.changeset/fn-8475-planning-status-badge.md
Normal file
@@ -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.
|
||||||
@@ -24,7 +24,7 @@ import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/project
|
|||||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection";
|
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection";
|
||||||
import { getRunningOptionalGateBadge, getRunningWorkflowStepLabel, getUnifiedTaskProgress } from "../utils/taskProgress";
|
import { getRunningOptionalGateBadge, getRunningWorkflowStepLabel, getUnifiedTaskProgress } from "../utils/taskProgress";
|
||||||
import { isTaskAgentActive } from "../utils/taskActivity";
|
import { isTaskAgentActive } from "../utils/taskActivity";
|
||||||
import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../utils/taskStatusBadgeLabel";
|
import { getTaskStatusBadgeLabel, hasTaskStatusBadge } from "../utils/taskStatusBadgeLabel";
|
||||||
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
||||||
import { useConfirm } from "../hooks/useConfirm";
|
import { useConfirm } from "../hooks/useConfirm";
|
||||||
import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete";
|
import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete";
|
||||||
@@ -2678,9 +2678,8 @@ export function ListView({
|
|||||||
&& !visualStatus
|
&& !visualStatus
|
||||||
&& Boolean(task.recentAgentActivityAt)
|
&& Boolean(task.recentAgentActivityAt)
|
||||||
&& isAgentActive;
|
&& isAgentActive;
|
||||||
const hasStatus = (typeof visualStatus === "string" && visualStatus.trim().length > 0
|
const hasStatus = (hasTaskStatusBadge(visualStatus) && visualStatus !== "queued")
|
||||||
|| isTransientPlannerActive)
|
|| isTransientPlannerActive;
|
||||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
|
||||||
const isReviewBudgetExhausted = isReviewBudgetExhaustedApproval(task);
|
const isReviewBudgetExhausted = isReviewBudgetExhaustedApproval(task);
|
||||||
const optionalGateBadge = getRunningOptionalGateBadge(task);
|
const optionalGateBadge = getRunningOptionalGateBadge(task);
|
||||||
const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive;
|
const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive;
|
||||||
@@ -2916,8 +2915,8 @@ export function ListView({
|
|||||||
&& !visualStatus
|
&& !visualStatus
|
||||||
&& Boolean(task.recentAgentActivityAt)
|
&& Boolean(task.recentAgentActivityAt)
|
||||||
&& isAgentActive;
|
&& isAgentActive;
|
||||||
const showStatusBadge = (Boolean(visualStatus) || isTransientPlannerActive)
|
const showStatusBadge = (hasTaskStatusBadge(visualStatus) && visualStatus !== "queued")
|
||||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
|| isTransientPlannerActive;
|
||||||
const optionalGateBadge = getRunningOptionalGateBadge(task);
|
const optionalGateBadge = getRunningOptionalGateBadge(task);
|
||||||
const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive;
|
const showOptionalGateBadge = Boolean(optionalGateBadge) && isAgentActive;
|
||||||
const isDragging = draggingTaskId === task.id;
|
const isDragging = draggingTaskId === task.id;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { getRunningOptionalGateBadge, getRunningWorkflowStepLabel, getUnifiedTas
|
|||||||
import { ACTIVE_STATUSES, isTaskAgentActive } from "../utils/taskActivity";
|
import { ACTIVE_STATUSES, isTaskAgentActive } from "../utils/taskActivity";
|
||||||
import { getPrBadgeModifierClass } from "../utils/prBadgeClass";
|
import { getPrBadgeModifierClass } from "../utils/prBadgeClass";
|
||||||
import { getTotalAgentActiveMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming";
|
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 { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval";
|
||||||
import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback";
|
import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
@@ -2973,9 +2973,8 @@ function TaskCardComponent({
|
|||||||
&& Boolean(task.recentAgentActivityAt)
|
&& Boolean(task.recentAgentActivityAt)
|
||||||
&& isAgentActive;
|
&& isAgentActive;
|
||||||
const showStatusBadge = !isPaused
|
const showStatusBadge = !isPaused
|
||||||
&& (Boolean(visualStatus) || isTransientPlannerActive)
|
&& (hasTaskStatusBadge(visualStatus) || isTransientPlannerActive)
|
||||||
&& visualStatus !== "queued"
|
&& visualStatus !== "queued";
|
||||||
&& !shouldSuppressPlanningStatusBadge({ status: visualStatus, column: task.column });
|
|
||||||
const hasCardMetaBadges = showPriorityBadge
|
const hasCardMetaBadges = showPriorityBadge
|
||||||
|| task.executionMode === "fast"
|
|| task.executionMode === "fast"
|
||||||
// FNXC:PlannerOversight 2026-07-04-00:00: the oversight badge is opt-in
|
// FNXC:PlannerOversight 2026-07-04-00:00: the oversight badge is opt-in
|
||||||
|
|||||||
@@ -2169,16 +2169,16 @@ describe("ListView", () => {
|
|||||||
expect(screen.queryByText("Reviewing")).not.toBeInTheDocument();
|
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();
|
const matchMediaSpy = mockDesktopViewport();
|
||||||
try {
|
try {
|
||||||
renderListView({
|
renderListView({
|
||||||
tasks: [
|
tasks: [
|
||||||
createMockTask({ id: "FN-8170-todo", column: "todo", status: "planning" }),
|
createMockTask({ id: "FN-8475-todo", column: "todo", status: "planning" }),
|
||||||
createMockTask({ id: "FN-8170-active", column: "in-progress", status: "planning" }),
|
createMockTask({ id: "FN-8475-active", column: "in-progress", status: "planning" }),
|
||||||
createMockTask({ id: "FN-8170-triage", column: "triage", status: "planning" }),
|
createMockTask({ id: "FN-8475-triage", column: "triage", status: "planning" }),
|
||||||
createMockTask({
|
createMockTask({
|
||||||
id: "FN-8170-executing",
|
id: "FN-8475-executing",
|
||||||
column: "in-progress",
|
column: "in-progress",
|
||||||
status: "executing",
|
status: "executing",
|
||||||
steps: [{ name: "Running step", status: "in-progress" }],
|
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;
|
const row = screen.getByText(id).closest("tr") as HTMLElement;
|
||||||
expect(within(row).queryByText("planning")).toBeNull();
|
expect(within(row).getByText("planning")).toHaveClass("list-status-badge");
|
||||||
expect(row.querySelector(".list-status-badge")).toHaveTextContent("-");
|
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-8475-executing").closest("tr") as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||||
expect(within(screen.getByText("FN-8170-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 {
|
} finally {
|
||||||
matchMediaSpy.mockRestore();
|
matchMediaSpy.mockRestore();
|
||||||
}
|
}
|
||||||
@@ -5170,7 +5183,7 @@ describe("ListView - Bulk Selection", () => {
|
|||||||
expect(within(card as HTMLElement).getByText("executing")).toBeInTheDocument();
|
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();
|
mockMobileViewport();
|
||||||
|
|
||||||
const { container } = renderListView({
|
const { container } = renderListView({
|
||||||
@@ -5187,10 +5200,9 @@ describe("ListView - Bulk Selection", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const id of ["FN-8170-mobile-todo", "FN-8170-mobile-active"]) {
|
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).queryByText("planning")).toBeNull();
|
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();
|
expect(within(container.querySelector('[data-id="FN-8170-mobile-executing"]') as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2295,7 +2295,8 @@ describe("TaskCard", () => {
|
|||||||
it.each([
|
it.each([
|
||||||
{ column: "todo" as const, status: "planning" },
|
{ column: "todo" as const, status: "planning" },
|
||||||
{ column: "in-progress" 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(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column, status })}
|
task={makeTask({ column, status })}
|
||||||
@@ -2304,11 +2305,12 @@ describe("TaskCard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByText("planning")).toBeNull();
|
const badge = screen.getByText("planning");
|
||||||
expect(container.querySelector(".card-header-badges")).toBeNull();
|
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(
|
const { rerender } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "triage", status: "planning" })}
|
task={makeTask({ column: "triage", status: "planning" })}
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { getTaskStatusBadgeLabel, shouldSuppressPlanningStatusBadge } from "../taskStatusBadgeLabel";
|
import { getTaskStatusBadgeLabel, hasTaskStatusBadge } from "../taskStatusBadgeLabel";
|
||||||
|
|
||||||
const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction<"app">;
|
const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction<"app">;
|
||||||
|
|
||||||
describe("shouldSuppressPlanningStatusBadge", () => {
|
describe("hasTaskStatusBadge", () => {
|
||||||
it.each([
|
it.each([
|
||||||
{ status: "planning", column: "todo", suppressed: true },
|
"planning",
|
||||||
{ status: "planning", column: "in-progress", suppressed: true },
|
"executing",
|
||||||
{ status: "planning", column: "triage", suppressed: false },
|
"reviewing",
|
||||||
{ status: "executing", column: "todo", suppressed: false },
|
"merging",
|
||||||
{ status: "executing", column: "in-progress", suppressed: false },
|
"failed",
|
||||||
{ status: "reviewing", column: "todo", suppressed: false },
|
"needs-replan",
|
||||||
{ status: "merging", column: "in-progress", suppressed: false },
|
"done",
|
||||||
{ status: "failed", column: "todo", suppressed: false },
|
])("keeps a real status visible regardless of column placement: %s", (status) => {
|
||||||
{ status: "needs-replan", column: "in-progress", suppressed: false },
|
expect(hasTaskStatusBadge(status)).toBe(true);
|
||||||
{ status: "done", column: "todo", suppressed: false },
|
});
|
||||||
{ status: null, column: "in-progress", suppressed: false },
|
|
||||||
{ status: undefined, column: "todo", suppressed: false },
|
it("leaves null, undefined, and empty status badge-free", () => {
|
||||||
])("suppresses only stale planning status for Todo and In Progress: $status/$column", ({ status, column, suppressed }) => {
|
expect(hasTaskStatusBadge(null)).toBe(false);
|
||||||
expect(shouldSuppressPlanningStatusBadge({ status, column })).toBe(suppressed);
|
expect(hasTaskStatusBadge(undefined)).toBe(false);
|
||||||
|
expect(hasTaskStatusBadge(" ")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,13 @@ import type { TFunction } from "i18next";
|
|||||||
import { isActiveMergeStatus } from "../../../core/src/active-merge-status";
|
import { isActiveMergeStatus } from "../../../core/src/active-merge-status";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TaskStatusBadge 2026-07-16-12:00:
|
FNXC:TaskStatusBadge 2026-08-19-00: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.
|
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({
|
export function hasTaskStatusBadge(status: string | null | undefined): boolean {
|
||||||
status,
|
return typeof status === "string" && status.trim().length > 0;
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
status?: string | null;
|
|
||||||
column: string;
|
|
||||||
}): boolean {
|
|
||||||
return status === "planning" && (column === "todo" || column === "in-progress");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskStatusBadgeLabel(
|
export function getTaskStatusBadgeLabel(
|
||||||
|
|||||||
Reference in New Issue
Block a user