fix(dashboard): restore accurate executing/total column headers

Show live agents over card count (e.g. 3/4) using the shared Running
predicate so unpaused WIP counts without sessionFile, and document the
badge semantics for operators.
This commit is contained in:
gsxdsm
2026-07-21 18:59:52 -07:00
parent 771fe71760
commit 2bf3d81a76
4 changed files with 60 additions and 9 deletions

View File

@@ -4,4 +4,4 @@
summary: Unify max concurrency across planning/execution/review and simplify board capacity indicators.
category: feature
dev: maxConcurrent caps all top-level working agents per project; maxTriageConcurrent removed from UI (Settings, Command Center, Engine Control) and admission; free slots admit oldest createdAt via per-project atomic admission coordinator across lanes; footer Waiting/Running/Blocked; column headers remain card totals; Running counts unpaused WIP membership (sessionFile is not a DB/board field — do not require it); nested runNested helpers remain parent-internal soft-breach by design.
dev: maxConcurrent caps all top-level working agents per project; maxTriageConcurrent removed from UI (Settings, Command Center, Engine Control) and admission; free slots admit oldest createdAt via per-project atomic admission coordinator across lanes; footer Waiting/Running/Blocked; column headers show executing/total via shared Running predicate; Running counts unpaused WIP membership (sessionFile is not a DB/board field — do not require it); nested runNested helpers remain parent-internal soft-breach by design.

View File

@@ -1258,7 +1258,7 @@ Features:
<!-- FNXC:CommandCenter 2026-06-27-10:03: Tokens detail charts must show every model bucket returned by analytics for accurate spend attribution; Overview remains a compact top-model summary because its copy explicitly frames those cards as top consumers/share. -->
<!-- FNXC:CommandCenterActivity 2026-06-30-00:00: Activity active-agent counts include both durable-agent usage events and ephemeral task-worker execution runs from agentRuns, because task execution can be visible without a matching usage_events row. -->
<!-- FNXC:CommandCenterActivity 2026-07-01-00:00: Graph-owned workflow step sessions publish active-to-terminal agentRuns lifecycle rows with task lineage and step metadata, so daily activity and Activity throughput charts include new workflow execution without dashboard-side recounting. -->
- **Overview controls dashboard** includes AI engine stop/start backed by `globalPause`, the shared Global Max Concurrent slider, and current-project **Max concurrency** plus **Max worktrees** controls. Max concurrency caps top-level working agents across planning, execution, and review/merge; free capacity is admitted oldest-first within the project. The footer reports **Waiting**, **Running (N/max)**, and **Blocked**; column headers report the card count in each lane. Nested helper agents remain parent-internal and may temporarily exceed the displayed top-level count.
- **Overview controls dashboard** includes AI engine stop/start backed by `globalPause`, the shared Global Max Concurrent slider, and current-project **Max concurrency** plus **Max worktrees** controls. Max concurrency caps top-level working agents across planning, execution, and review/merge; free capacity is admitted oldest-first within the project. The footer reports **Waiting**, **Running (N/max)**, and **Blocked**; column headers report executing/total (live agents in the lane over card count). Nested helper agents remain parent-internal and may temporarily exceed the displayed top-level count.
<!-- FNXC:TeamArea 2026-07-18-12:30: FN-8351 moves organization export and import to the Team tab so team-level portability controls are not presented as Overview dashboard controls. -->
- **Team tab — Org export / import** lets an operator download a portable organization JSON bundle or paste one for a dry-run preview before confirming the apply step. Exports are secret-scrubbed by default: credentials and tokens are never included, while safe secret references can remain for setup in the destination project.
- **Configuration versions** lives in **Settings → Project → Configuration Versions**. It lists recorded project-setting revisions newest first; select **Roll back** on any revision and confirm once to restore it. The restore is recorded as a new forward revision, so it can itself be undone without manually reconstructing settings.

View File

@@ -4,6 +4,7 @@ import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
import { useConfirm } from "../hooks/useConfirm";
import type { Task, TaskDetail, Column as ColumnType, ColumnId, TaskCreateInput, GithubIssueAction, MergeResult } from "@fusion/core";
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS, getErrorMessage } from "@fusion/core";
import { enrichRunningAgentTaskShapeFromFlags, isRunningAgentTask } from "../../../core/src/live-agent-count";
import { isNearDuplicateCanonicalInactive } from "../../../core/src/near-duplicate-canonical";
import { TaskCard } from "./TaskCard";
import { WorktreeGroup } from "./WorktreeGroup";
@@ -287,10 +288,14 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
*/
const showWorktreeGroups = showWorktreeGrouping === true && isWipProcessingColumn;
/*
FNXC:BoardColumnCount 2026-07-21-18:47:
Column headers show the card count in that lane, not live-agent active/total.
FN-8453 briefly rendered isRunningAgentTask counts as N/total, which read as broken for Todo (always 0/N waiting) and often for In Progress when board slim rows lack durable session/checkout liveness. Capacity Running/Waiting stays on the footer status bar.
FNXC:BoardColumnCount 2026-07-21-19:30:
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.
*/
const activeTaskCount = useMemo(
() => tasks.filter((task) => isRunningAgentTask(enrichRunningAgentTaskShapeFromFlags(task, columnFlags))).length,
[tasks, columnFlags],
);
// When search is active, skip pagination so all matching tasks are visible
const shouldPaginate = !isArchived && !isSearchActive && !showWorktreeGroups && tasks.length > PAGINATED_COLUMN_THRESHOLD;
@@ -661,7 +666,12 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
<div className="column-header">
<div className={`column-dot dot-${column}`} />
<h2>{workflowMode ? (columnDisplayName ?? COLUMN_LABELS[column] ?? column) : COLUMN_LABELS[column]}</h2>
<span className={`column-count${countFlashing ? " count-flash" : ""}`}>{tasks.length}</span>
<span
className={`column-count${countFlashing ? " count-flash" : ""}`}
aria-label={t("column.executingOfTotal", "{{active}} executing of {{total}}", { active: activeTaskCount, total: tasks.length })}
>
<span>{activeTaskCount}</span>/<span>{tasks.length}</span>
</span>
{(workflowMode ? isReviewColumn : column === "in-review") && onToggleAutoMerge && (
<label className="auto-merge-toggle" title={autoMerge ? t("column.autoMergeEnabled", "Auto-merge enabled") : t("column.autoMergeDisabled", "Auto-merge disabled")}>
{/*

View File

@@ -109,9 +109,11 @@ describe("Column count-flash", () => {
const tasks = [makeTask("FN-001")];
render(<Column {...defaultProps} tasks={tasks} />);
const badge = screen.getByText("1");
// Badge is active/total (0/1 for triage without a live planner).
const badge = screen.getByText("1").parentElement!;
expect(badge.className).toContain("column-count");
expect(badge.className).not.toContain("count-flash");
expect(badge).toHaveTextContent("0/1");
});
it("applies count-flash class when task count increases", () => {
@@ -121,8 +123,9 @@ describe("Column count-flash", () => {
const moreTasks = [makeTask("FN-001"), makeTask("FN-002")];
rerender(<Column {...defaultProps} tasks={moreTasks} />);
const badge = screen.getByText("2");
const badge = screen.getByText("2").parentElement!;
expect(badge.className).toContain("count-flash");
expect(badge).toHaveTextContent("0/2");
});
it("does not apply count-flash class when task count decreases", () => {
@@ -132,9 +135,47 @@ describe("Column count-flash", () => {
const fewerTasks = [makeTask("FN-001")];
rerender(<Column {...defaultProps} tasks={fewerTasks} />);
const badge = screen.getByText("1");
const badge = screen.getByText("1").parentElement!;
expect(badge.className).not.toContain("count-flash");
});
it("shows accurate executing/total for WIP (unpaused active over card total)", () => {
const tasks = [
{ ...makeTask("FN-001"), column: "in-progress" as ColumnType },
{ ...makeTask("FN-002"), column: "in-progress" as ColumnType },
{ ...makeTask("FN-003"), column: "in-progress" as ColumnType, paused: true },
{ ...makeTask("FN-004"), column: "in-progress" as ColumnType, userPaused: true },
];
render(
<Column
{...defaultProps}
column={"in-progress" as ColumnType}
columnFlags={{ countsTowardWip: true }}
tasks={tasks}
/>,
);
expect(screen.getByLabelText("2 executing of 4")).toHaveTextContent("2/4");
});
it("counts only live planners as executing in todo (queued is not executing)", () => {
const tasks = [
{ ...makeTask("FN-001"), column: "todo" as ColumnType, status: "planning" as any },
{ ...makeTask("FN-002"), column: "todo" as ColumnType, status: "queued" as any },
{ ...makeTask("FN-003"), column: "todo" as ColumnType, status: "queued" as any },
{ ...makeTask("FN-004"), column: "todo" as ColumnType, status: "queued" as any },
];
render(
<Column
{...defaultProps}
column={"todo" as ColumnType}
columnFlags={{ hold: true }}
tasks={tasks}
/>,
);
expect(screen.getByLabelText("1 executing of 4")).toHaveTextContent("1/4");
});
});
describe("Column workflow mode (U9)", () => {