fix(dashboard): count visibly active cards (REVISING replans) in column header processing count

A Todo card parked in the durable needs-replan stage keeps its REVISING badge
and activity chrome (FN-8494), but the header only counted live agents, so it
read 0/2 under a glowing card. Union the shared Running predicate with the
card's own activity predicate (same globalPaused/stuck gates) so the header
equals the number of visibly active cards. Footer Running and admission keep
live-agent-only truth: a parked replan must not consume concurrency capacity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-22 18:01:34 -07:00
parent 295226ea64
commit 3f7c6e4848
3 changed files with 61 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Board column headers now count REVISING (replan) cards and other visibly active cards in the processing count.
category: fix
dev: Column header count = shared Running predicate ∪ card activity-chrome predicate (isTaskAgentActive); footer/admission keep live-agent-only semantics.

View File

@@ -11,6 +11,8 @@ import { WorktreeGroup } from "./WorktreeGroup";
import { QuickEntryBox } from "./QuickEntryBox"; import { QuickEntryBox } from "./QuickEntryBox";
import { PluginSlot } from "./PluginSlot"; import { PluginSlot } from "./PluginSlot";
import { groupByWorktree } from "../utils/worktreeGrouping"; import { groupByWorktree } from "../utils/worktreeGrouping";
import { isTaskAgentActive } from "../utils/taskActivity";
import { isTaskStuck } from "../utils/taskStuck";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import type { TaskContextMenuColumnMetadata } from "./TaskContextMenu"; import type { TaskContextMenuColumnMetadata } from "./TaskContextMenu";
import { ChevronDown, ChevronUp, MoreVertical } from "lucide-react"; import { ChevronDown, ChevronUp, MoreVertical } from "lucide-react";
@@ -291,10 +293,22 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
FNXC:BoardColumnCount 2026-07-21-19:30: FNXC:BoardColumnCount 2026-07-21-19:30:
Column header is executing/total (e.g. 3/4). Executing uses the same Running predicate as the Column header is executing/total (e.g. 3/4). Executing uses the same Running predicate as the
footer (unpaused WIP, live planners, active review). Total is the card count in this lane. footer (unpaused WIP, live planners, active review). Total is the card count in this lane.
FNXC:BoardColumnCount 2026-07-22-06:10:
The header must agree with the cards below it: a Todo card parked in the durable
`needs-replan` stage keeps its REVISING badge and activity chrome (FN-8494), so a header
that only counts live agents read 0/2 under a glowing card. Union the shared Running
predicate with the card's own activity-chrome predicate (isTaskAgentActive, same
globalPaused/stuck gates the card applies) so the count equals the number of visibly
active cards. Footer Running and admission intentionally keep the live-agent-only truth —
a parked replan must not consume top-level concurrency capacity.
*/ */
const activeTaskCount = useMemo( const activeTaskCount = useMemo(
() => tasks.filter((task) => isRunningAgentTask(enrichRunningAgentTaskShapeFromFlags(task, columnFlags))).length, () => tasks.filter((task) =>
[tasks, columnFlags], isRunningAgentTask(enrichRunningAgentTaskShapeFromFlags(task, columnFlags))
|| isTaskAgentActive(task, { globalPaused, isStuck: isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs) }),
).length,
[tasks, columnFlags, globalPaused, taskStuckTimeoutMs, lastFetchTimeMs],
); );
// When search is active, skip pagination so all matching tasks are visible // When search is active, skip pagination so all matching tasks are visible
const shouldPaginate = !isArchived && !isSearchActive && !showWorktreeGroups && tasks.length > PAGINATED_COLUMN_THRESHOLD; const shouldPaginate = !isArchived && !isSearchActive && !showWorktreeGroups && tasks.length > PAGINATED_COLUMN_THRESHOLD;

View File

@@ -179,6 +179,44 @@ describe("Column count-flash", () => {
expect(screen.getByLabelText("1 executing of 4")).toHaveTextContent("1/4"); expect(screen.getByLabelText("1 executing of 4")).toHaveTextContent("1/4");
}); });
it("counts cards with active chrome — a REVISING (needs-replan) todo card and a live code-review gate", () => {
// Header must equal the number of visibly active cards (FN-8494 keeps REVISING chrome on
// parked replans; a live gate session runs with null status and a pending step lease).
const tasks = [
{ ...makeTask("FN-001"), column: "todo" as ColumnType, status: "needs-replan" as any },
{ ...makeTask("FN-002"), column: "todo" as ColumnType },
];
render(
<Column
{...defaultProps}
column={"todo" as ColumnType}
columnFlags={{ hold: true }}
tasks={tasks}
/>,
);
expect(screen.getByLabelText("1 executing of 2")).toHaveTextContent("1/2");
});
it("counts an in-review card whose code-review gate holds a pending step lease", () => {
const tasks = [
{
...makeTask("FN-001"),
column: "in-review" as ColumnType,
workflowStepResults: [{ workflowStepId: "code-review", workflowStepName: "Code Review", status: "pending" as const, startedAt: new Date().toISOString() }],
},
{ ...makeTask("FN-002"), column: "in-review" as ColumnType },
];
render(
<Column
{...defaultProps}
column={"in-review" as ColumnType}
columnFlags={{ mergeBlocker: true }}
tasks={tasks}
/>,
);
expect(screen.getByLabelText("1 executing of 2")).toHaveTextContent("1/2");
});
}); });
/* /*