From fc07bdfc7eba72350837cd508f28fc5aad3ea2ab Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 20:37:26 -0700 Subject: [PATCH] FN-7831: add Reviewing badge for active Plan Review on task cards and list rows Adds a distinct "Reviewing" status badge that surfaces on TaskCard and ListView rows while a task's optional plan-review workflow step is actively running, reusing the unified progress predicate so board and list surfaces stay in sync. - Add isPlanReviewRunning(task) helper in taskProgress.ts, derived from getUnifiedTaskProgress's workflow-plan-review item status - Render a pulsing "Reviewing" badge on TaskCard header (additive to existing status badges, with title/data-testid) while plan-review is running - Render the matching "Reviewing" badge on both grouped and ungrouped ListView row layouts for parity with TaskCard - Add supporting CSS for .card-status-badge--reviewing and .list-status-badge--reviewing - Add unit tests for isPlanReviewRunning and component tests for the new badge across TaskCard and ListView - Add minor changeset documenting the new operator-facing badge Files changed: .changeset/tidy-reviewing-badges.md | 7 ++ packages/dashboard/app/components/ListView.css | 10 +++ packages/dashboard/app/components/ListView.tsx | 22 +++++- packages/dashboard/app/components/TaskCard.css | 10 +++ packages/dashboard/app/components/TaskCard.tsx | 25 ++++++- .../app/components/__tests__/ListView.test.tsx | 78 ++++++++++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 31 +++++++++ .../app/utils/__tests__/taskProgress.test.ts | 16 ++++- packages/dashboard/app/utils/taskProgress.ts | 10 +++ 9 files changed, 205 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7831 Fusion-Task-Lineage: d36f8c63-9b84-400a-8b10-3b9f3b04212b Co-authored-by: Fusion (runfusion.ai) --- .changeset/tidy-reviewing-badges.md | 7 ++ .../dashboard/app/components/ListView.css | 10 +++ .../dashboard/app/components/ListView.tsx | 22 +++++- .../dashboard/app/components/TaskCard.css | 10 +++ .../dashboard/app/components/TaskCard.tsx | 25 +++++- .../components/__tests__/ListView.test.tsx | 78 +++++++++++++++++++ .../components/__tests__/TaskCard.test.tsx | 31 ++++++++ .../app/utils/__tests__/taskProgress.test.ts | 16 +++- packages/dashboard/app/utils/taskProgress.ts | 10 +++ 9 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 .changeset/tidy-reviewing-badges.md diff --git a/.changeset/tidy-reviewing-badges.md b/.changeset/tidy-reviewing-badges.md new file mode 100644 index 0000000000..223baea42a --- /dev/null +++ b/.changeset/tidy-reviewing-badges.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Task cards now show a "Reviewing" badge while a task is in Plan Review. +category: feature +dev: Adds isPlanReviewRunning(task) in taskProgress.ts; consumed by TaskCard + ListView status badges. diff --git a/packages/dashboard/app/components/ListView.css b/packages/dashboard/app/components/ListView.css index 89bf5f06e7..807dd5f69f 100644 --- a/packages/dashboard/app/components/ListView.css +++ b/packages/dashboard/app/components/ListView.css @@ -741,6 +741,16 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h background: var(--status-in-review-bg); color: var(--in-review); } + +/* +FNXC:TaskCardPlanReviewBadge 2026-07-11-12:16: +ListView uses the same active-review token as TaskCard for the FN-7831 Reviewing badge. Keep the modifier styling token-only and geometry-neutral so grouped cards and table rows preserve existing responsive layout. +*/ +.list-status-badge--reviewing { + background: color-mix(in srgb, var(--in-review) 18%, transparent); + color: var(--in-review); + box-shadow: inset 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--in-review) 35%, transparent); +} .list-status-badge--done { background: var(--status-done-bg); color: var(--done); diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index a164aa4f8f..461381612d 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -21,7 +21,7 @@ import type { ToastType } from "../hooks/useToast"; import { useViewportMode } from "../hooks/useViewportMode"; import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage"; import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection"; -import { getUnifiedTaskProgress } from "../utils/taskProgress"; +import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgress"; import { useConfirm } from "../hooks/useConfirm"; import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete"; import { WorkflowSwitcher } from "./WorkflowSwitcher"; @@ -2596,6 +2596,7 @@ export function ListView({ !isStuckState && (task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string)); const hasStatus = typeof visualStatus === "string" && visualStatus.trim().length > 0; + const planReviewRunning = isPlanReviewRunning(task); const hasDependencies = Boolean(task.dependencies && task.dependencies.length > 0); const taskProgress = getTaskProgress(task); const hasProgress = taskProgress.hasProgress; @@ -2654,6 +2655,15 @@ export function ListView({ {getTaskStatusLabel(visualStatus ?? "", t)} ) : null} + {planReviewRunning && ( + /* + FNXC:TaskCardPlanReviewBadge 2026-07-11-12:10: + Grouped ListView cards must show the same active Plan Review "Reviewing" badge as TaskCard so board and list surfaces remain visually equivalent while the `plan-review` workflow step is running. + */ + + {t("listView.reviewing", "Reviewing")} + + )}
@@ -2796,6 +2806,7 @@ export function ListView({ !isPaused && !isStuckState && (task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string)); + const planReviewRunning = isPlanReviewRunning(task); const isDragging = draggingTaskId === task.id; return ( @@ -2870,6 +2881,15 @@ export function ListView({ ) : ( - )} + {planReviewRunning && ( + /* + FNXC:TaskCardPlanReviewBadge 2026-07-11-12:11: + Ungrouped ListView table rows must render the same Reviewing badge from the shared predicate; this second status render path is easy to miss and must stay in parity with grouped rows. + */ + + {t("listView.reviewing", "Reviewing")} + + )} )} {visibleColumns.has("column") && ( diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index b019135634..e7e952bcca 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -292,6 +292,16 @@ FN-7780 anchors the created-by-agent badge in its own bottom-left row before wor color: var(--color-error); } +/* +FNXC:TaskCardPlanReviewBadge 2026-07-11-12:15: +The Plan Review "Reviewing" badge must read as an active review state without adding custom sizing. Reuse the in-review semantic token and existing status badge geometry so desktop and mobile header wrapping keep the same invariants. +*/ +.card-status-badge--reviewing { + background: color-mix(in srgb, var(--in-review) 18%, transparent); + color: var(--in-review); + border-color: color-mix(in srgb, var(--in-review) 35%, transparent); +} + .card-status-badge.stalled-review { background: color-mix(in srgb, var(--color-warning) 18%, transparent); color: var(--color-warning); diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index b6e4c436a0..5ca8af1df1 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -39,7 +39,7 @@ import { getStalledReviewSignal } from "../utils/taskStalledReview"; import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../utils/inReviewStallCopy"; import { getStalePausedReviewCopy, shouldShowStalePausedReviewBadge } from "../utils/stalePausedReviewCopy"; import { getTaskAgeStalenessCopy, shouldShowTaskAgeStalenessBadge } from "../utils/taskAgeStalenessCopy"; -import { getUnifiedTaskProgress } from "../utils/taskProgress"; +import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgress"; import { getPrBadgeModifierClass } from "../utils/prBadgeClass"; import { getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming"; import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback"; @@ -1274,6 +1274,14 @@ function TaskCardComponent({ const stalledReview = getStalledReviewSignal(task); const showStalledReview = Boolean(stalledReview && task.column === "in-review" && !isPaused); const hasInReviewStall = shouldShowInReviewStallBadge(task); + /* + FNXC:TaskCardPlanReviewBadge 2026-07-11-12:05: + FN-7831 requires the card header to show a distinct "Reviewing" badge while the optional `plan-review` workflow step is actively running, even while the card remains in Planning/`triage`. Use the shared predicate so TaskCard stays in sync with ListView. + */ + const planReviewRunning = useMemo( + () => isPlanReviewRunning(task), + [task.steps, task.enabledWorkflowSteps, task.workflowStepResults], + ); // CLI agent session badges (U11) — distinct from staleness/stall badges. const cliWaitingOnInput = cliSessionState?.agentState === "waitingOnInput"; const cliNeedsAttention = cliSessionState?.agentState === "needsAttention"; @@ -1389,7 +1397,7 @@ function TaskCardComponent({ ); /* FNXC:TaskCardWorkflowProgress 2026-07-08-hh:mm: - FN-7676 — cards in the Planning/`triage` column must not surface the steps breakdown (progress bar, active badge, step-count toggle, expandable list); enumerated implementation steps are premature planning artifacts, not execution progress. The affordance now appears only after the task leaves Planning (`in-progress` / `executing`), matching `ListView.shouldShowTaskProgress`. A running Plan Review while still in `triage` intentionally no longer surfaces the card progress indicator — the header `planning` status badge remains the only in-flight signal. + FN-7676 — cards in the Planning/`triage` column must not surface the steps breakdown (progress bar, active badge, step-count toggle, expandable list); enumerated implementation steps are premature planning artifacts, not execution progress. The affordance now appears only after the task leaves Planning (`in-progress` / `executing`), matching `ListView.shouldShowTaskProgress`. FN-7831 adds a separate header "Reviewing" badge for a running Plan Review, but the progress breakdown itself remains hidden in Planning. */ const showProgressSection = unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress"); @@ -2840,6 +2848,19 @@ function TaskCardComponent({ {isStuck ? t("tasks.stuck", "Stuck") : isAwaitingApproval ? t("tasks.awaitingApproval", "Awaiting Approval") : isAwaitingInput ? t("tasks.needsInput", "Needs input") : visualStatus === "merging-fix" ? t("tasks.statusMergingFix", "Merging fixes…") : getTaskStatusLabel(visualStatus, t)} )} + {planReviewRunning && ( + /* + FNXC:TaskCardPlanReviewBadge 2026-07-11-12:06: + The Reviewing badge is additive to the normal header status badge so operators can distinguish "planning" from active Plan Review without hiding paused/stuck/status affordances. + */ + + {t("tasks.reviewing", "Reviewing")} + + )} {/* FNXC:CodingIdeasWorkflow 2026-07-04-11:10: In the merged planner/capacity "todo" column (Coding (Ideas)), a planned task with no active status is ready and waiting for an in-progress slot. Show a "Ready" badge so operators can distinguish planned cards from freshly promoted unplanned ones. Tasks still being planned surface the "planning" status badge above instead. diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx index 5a78814803..8c1e44184e 100644 --- a/packages/dashboard/app/components/__tests__/ListView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx @@ -1865,6 +1865,84 @@ describe("ListView", () => { } }); + it("shows the Reviewing badge in the desktop table status cell while Plan Review runs", () => { + const tasks = [ + createMockTask({ + id: "FN-7831", + status: "planning", + enabledWorkflowSteps: ["plan-review"], + workflowStepResults: [ + { + workflowStepId: "plan-review", + workflowStepName: "Plan Review", + status: "pending", + startedAt: "2026-07-11T12:00:00.000Z", + }, + ], + } as Partial), + ]; + + renderListView({ tasks }); + + const row = screen.getByText("FN-7831").closest("tr"); + expect(row).not.toBeNull(); + expect(within(row as HTMLElement).getByText("Reviewing")).toBeInTheDocument(); + expect(within(row as HTMLElement).getByText("planning")).toBeInTheDocument(); + }); + + it("shows the Reviewing badge in grouped mobile cards while Plan Review runs", () => { + const matchMediaSpy = mockMobileViewport(); + try { + const tasks = [ + createMockTask({ + id: "FN-7831", + status: "planning", + enabledWorkflowSteps: ["plan-review"], + workflowStepResults: [ + { + workflowStepId: "plan-review", + workflowStepName: "Plan Review", + status: "pending", + startedAt: "2026-07-11T12:00:00.000Z", + }, + ], + } as Partial), + ]; + + renderListView({ tasks }); + + const card = screen.getByText("FN-7831").closest(".list-card"); + expect(card).not.toBeNull(); + expect(within(card as HTMLElement).getByText("Reviewing")).toBeInTheDocument(); + expect(within(card as HTMLElement).getByText("planning")).toBeInTheDocument(); + } finally { + matchMediaSpy.mockRestore(); + } + }); + + it("does not show the Reviewing badge after Plan Review completes", () => { + const tasks = [ + createMockTask({ + id: "FN-7831", + status: "planning", + enabledWorkflowSteps: ["plan-review"], + workflowStepResults: [ + { + workflowStepId: "plan-review", + workflowStepName: "Plan Review", + status: "passed", + startedAt: "2026-07-11T12:00:00.000Z", + completedAt: "2026-07-11T12:01:00.000Z", + }, + ], + } as Partial), + ]; + + renderListView({ tasks }); + + expect(screen.queryByText("Reviewing")).not.toBeInTheDocument(); + }); + it("renders paused tasks with dimmed styling", () => { const tasks = [createMockTask({ id: "FN-001", paused: true })]; diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index a4cc47d519..aa7442f652 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -1986,6 +1986,37 @@ describe("TaskCard", () => { } }); + it.each([ + { name: "undefined results", workflowStepResults: undefined, shouldRender: false }, + { name: "pending but not started", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending" }], shouldRender: false }, + { name: "running", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending", startedAt: "2026-07-11T12:00:00.000Z" }], shouldRender: true }, + { name: "passed", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "passed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false }, + { name: "failed", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "failed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false }, + { name: "skipped", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "skipped", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false }, + { name: "advisory failure", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "advisory_failure", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false }, + ])("renders the Reviewing badge only while Plan Review is actively running: $name", ({ workflowStepResults, shouldRender }) => { + const { container } = render( + , + ); + + const badge = container.querySelector('[data-testid="card-reviewing-FN-7831"]'); + expect(Boolean(badge)).toBe(shouldRender); + if (shouldRender) { + expect(badge).toHaveTextContent("Reviewing"); + expect(screen.getByText("planning")).toBeDefined(); + } + }); + it("renders the status badge after the card ID in DOM order", () => { const { container } = render( ; } +describe("isPlanReviewRunning", () => { + it.each([ + { name: "undefined results", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: undefined }, expected: false }, + { name: "pending but not started", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending" }] }, expected: false }, + { name: "started and not completed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending", startedAt: "2026-07-11T12:00:00.000Z" }] }, expected: true }, + { name: "passed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "passed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false }, + { name: "failed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "failed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false }, + { name: "skipped", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "skipped", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false }, + { name: "advisory failure", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "advisory_failure", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false }, + ])("returns $expected for $name", ({ task, expected }) => { + expect(isPlanReviewRunning(makeTask(task as Partial>))).toBe(expected); + }); +}); + describe("getUnifiedTaskProgress", () => { it("resolves workflow step names from result.workflowStepName without a lookup", () => { const progress = getUnifiedTaskProgress( diff --git a/packages/dashboard/app/utils/taskProgress.ts b/packages/dashboard/app/utils/taskProgress.ts index ace87d3fb4..7008052593 100644 --- a/packages/dashboard/app/utils/taskProgress.ts +++ b/packages/dashboard/app/utils/taskProgress.ts @@ -139,3 +139,13 @@ export function getUnifiedTaskProgress( return { total, completed, items }; } + +/* +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. +*/ +export function isPlanReviewRunning(task: Pick): boolean { + return getUnifiedTaskProgress(task).items.some( + (item) => item.id === "workflow-plan-review" && item.status === "running", + ); +}