FN-7780: move agent-created badge to bottom row on task cards

Moves the agent-created badge out of the task card header's meta-badges row into its own bottom row so it no longer causes header wrapping.

- Moved .card-agent-created-badge out of .card-meta-badges into a new .card-agent-badge-row in TaskCard.tsx
- Updated hasCardMetaBadges guard logic and TaskCard.css to support the new bottom row layout
- Added TaskCard.badge-wrap.test.tsx regression coverage and extended TaskCard.test.tsx
- Updated docs/dashboard-guide.md
- Added changeset fn-7780-agent-badge-bottom-row.md (patch, fix category)

Files changed:
 .changeset/fn-7780-agent-badge-bottom-row.md       |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 packages/dashboard/app/components/TaskCard.css     | 12 ++++
 packages/dashboard/app/components/TaskCard.tsx     | 29 ++++++----
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 66 ++++++++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 34 ++++++++---
 6 files changed, 131 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-7780

Fusion-Task-Lineage: 6a6cd9f6-a9ce-488f-969d-520459c8631b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-10 08:06:08 -07:00
parent 5729fe292c
commit f5fd8b84f8
6 changed files with 131 additions and 20 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task cards no longer wrap the header when a task was created by an agent — the agent badge moved to a bottom row.
category: fix
dev: Moved `.card-agent-created-badge` out of `.card-meta-badges` into a new `.card-agent-badge-row` in TaskCard; updated the `hasCardMetaBadges` guard.

View File

@@ -193,7 +193,8 @@ Features:
- PR/issue badges with live updates
- GitLab tracking badges on task cards for linked GitLab project issues, group issues, and merge requests; stale GitLab metadata uses a warning-colored badge while GitHub badges remain unchanged.
- GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown in the footer with other external-source metadata
- Task card header meta badges group priority, fast mode, agent-created provenance, workflow identity, and elapsed/created-time chips into one wrapping row; agent labels prefer `sourceMetadata.agentName` over raw agent IDs
- Task card header meta badges group priority and fast mode in the header; agent-created provenance renders in a dedicated bottom-left row ahead of workflow identity so the ID/status/actions header does not wrap on narrow cards. Agent labels prefer `sourceMetadata.agentName` over raw agent IDs.
<!-- FNXC:TaskCardLayout 2026-07-10-00:00: FN-7780 moved agent-created provenance out of the header meta-badge cluster into a bottom row. Keep dashboard docs aligned so operators do not expect the agent chip to participate in header wrapping. -->
<!-- FNXC:PlannerOversight 2026-07-04-00:00: FN-7516 adds a read-only effective oversight-level badge plus an active-overseer-state indicator to the card-meta-badges cluster. The overseer-state indicator is derived card-locally from already-on-Task fields (mirroring the engine's stage-resolution precedence) rather than a new engine-plumbed field, since @fusion/engine's in-memory monitor state is not persisted onto Task/exposed via API. -->
<!-- FNXC:PlannerOversight 2026-07-04-16:00: round-2 code-review fix — when a card must fetch the workflow's effective oversight tier (no synchronous per-task override), neither badge below renders until that fetch resolves; the schema default must never render as a guess while the true workflow tier is unknown. -->
<!-- FNXC:PlannerOversight 2026-07-04-19:10: FN-7539 fix — the badge was rendering on virtually every card because the schema default `autonomous` tier was treated as "meaningfully configured". Narrowed the gate so an inherited (no per-task-override, no non-default workflow tier) `autonomous` level renders no badge; only an explicit per-task override or a resolved workflow/effective tier that is not the plain inherited default surfaces the badge. -->

View File

@@ -234,6 +234,18 @@ All workflows cards need workflow identity at the bottom-left below every other
FNXC:WorkflowBadges 2026-06-30-00:00:
Workflow badges need a slight token-based gap between icon and label so compact workflow identity remains readable without changing placement, clickability, or truncation behavior.
*/
/*
FNXC:TaskCardLayout 2026-07-10-00:00:
FN-7780 anchors the created-by-agent badge in its own bottom-left row before workflow identity, matching the existing workflow-row layout so the header ID/status/action row no longer wraps because of the agent chip on narrow/mobile cards.
*/
.card-agent-badge-row {
display: flex;
align-items: center;
justify-content: flex-start;
min-width: 0;
margin-top: var(--space-xs);
}
.card-workflow-badge-row {
display: flex;
align-items: center;

View File

@@ -2704,7 +2704,6 @@ function TaskCardComponent({
*/
const hasCardMetaBadges = showPriorityBadge
|| task.executionMode === "fast"
|| isAgentCreated
// FNXC:PlannerOversight 2026-07-04-00:00: the oversight badge is opt-in
// metadata (absent for the common "off" default) — include it in the wrapper
// guard so `.card-meta-badges` only renders when it has a real child.
@@ -2983,17 +2982,6 @@ function TaskCardComponent({
<span className="visually-hidden">{t("tasks.fastMode", "Fast mode")}</span>
</span>
)}
{isAgentCreated && (
<span
className="card-agent-created-badge"
title={agentCreatedTitle}
aria-label={agentCreatedTitle}
>
<Bot size={11} aria-hidden="true" />
<span className="visually-hidden">{agentCreatedTitle}</span>
<span aria-hidden="true">{agentCreatedVisibleLabel}</span>
</span>
)}
{showOversightBadge && (
<span
className={`card-oversight-badge card-oversight-badge--${OVERSIGHT_BADGE_MODIFIER[effectiveOversightLevel as Exclude<PlannerOversightLevel, "off">]}`}
@@ -3544,6 +3532,23 @@ function TaskCardComponent({
{showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()}
</div>
)}
{isAgentCreated && (
<div className="card-agent-badge-row" data-testid="card-agent-badge-row">
{/**
* FNXC:TaskCardLayout 2026-07-10-00:00:
* FN-7780 moves the created-by-agent chip below the task content and before the workflow identity row so the header keeps only ID/status/actions metadata and no longer wraps on narrow/mobile cards. The badge content, tooltip, and accessible name remain unchanged.
*/}
<span
className="card-agent-created-badge"
title={agentCreatedTitle}
aria-label={agentCreatedTitle}
>
<Bot size={11} aria-hidden="true" />
<span className="visually-hidden">{agentCreatedTitle}</span>
<span aria-hidden="true">{agentCreatedVisibleLabel}</span>
</span>
</div>
)}
{hasWorkflowBadge && (
<div className="card-workflow-badge-row" data-testid="card-workflow-badge-row">
{/*

View File

@@ -123,15 +123,81 @@ describe("TaskCard badge wrapping (FN-5162)", () => {
expect(styles.whiteSpace).toBe("nowrap");
});
it("places the agent badge in a left-aligned bottom row outside the header badge cluster", () => {
const agentRow = container.querySelector(".card-agent-badge-row") as HTMLElement;
const agentBadge = container.querySelector(".card-agent-created-badge") as HTMLElement;
const header = container.querySelector(".card-header") as HTMLElement;
const metaBadges = container.querySelector(".card-meta-badges") as HTMLElement;
expect(agentRow).toBeTruthy();
expect(agentBadge).toBeTruthy();
expect(agentRow.contains(agentBadge)).toBe(true);
expect(header.contains(agentBadge)).toBe(false);
expect(metaBadges.contains(agentBadge)).toBe(false);
expect(agentBadge.getAttribute("title")).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.getAttribute("aria-label")).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.querySelector(".visually-hidden")?.textContent).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.querySelector("span[aria-hidden='true']")?.textContent).toBe("agent-badge-...");
const styles = getComputedStyle(agentRow);
expect(styles.display).toBe("flex");
expect(styles.justifyContent).toBe("flex-start");
expect(styles.minWidth).toBe("0px");
});
it("does not render an empty header badge shell when the agent badge is the only grouped affordance", () => {
const { container: agentOnlyContainer } = render(
<TaskCard
task={makeTask({
priority: "normal" as Task["priority"],
executionMode: "standard",
sourceType: "agent_heartbeat",
sourceAgentId: "agent-only",
plannerOversightLevel: "off",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const agentBadge = agentOnlyContainer.querySelector(".card-agent-created-badge");
expect(agentBadge).not.toBeNull();
expect(agentBadge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(agentBadge?.closest(".card-header")).toBeNull();
expect(agentOnlyContainer.querySelector(".card-meta-badges")).toBeNull();
});
it("omits the agent bottom row for non-agent-created tasks", () => {
const { container: nonAgentContainer } = render(
<TaskCard
task={makeTask({
priority: "normal" as Task["priority"],
executionMode: "standard",
sourceType: "dashboard_ui",
sourceAgentId: undefined,
plannerOversightLevel: "off",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(nonAgentContainer.querySelector(".card-agent-created-badge")).toBeNull();
expect(nonAgentContainer.querySelector(".card-agent-badge-row")).toBeNull();
expect(nonAgentContainer.querySelector(".card-meta-badges")).toBeNull();
});
it("places the workflow badge in a left-aligned bottom row outside the header badge cluster", () => {
const workflowRow = container.querySelector(".card-workflow-badge-row") as HTMLElement;
const workflowBadge = container.querySelector(".card-workflow-badge") as HTMLElement;
const metaBadges = container.querySelector(".card-meta-badges") as HTMLElement;
const agentRow = container.querySelector(".card-agent-badge-row") as HTMLElement;
expect(workflowRow).toBeTruthy();
expect(workflowBadge).toBeTruthy();
expect(workflowRow.contains(workflowBadge)).toBe(true);
expect(metaBadges.contains(workflowBadge)).toBe(false);
expect(agentRow.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
const styles = getComputedStyle(workflowRow);
expect(styles.display).toBe("flex");

View File

@@ -2594,7 +2594,7 @@ describe("TaskCard", () => {
});
it("keeps priority, fast mode, and agent-created in meta while time moves to footer", () => {
it("keeps priority and fast mode in meta while agent-created and time move to bottom rows", () => {
const { container } = render(
<TaskCard
@@ -2618,7 +2618,6 @@ describe("TaskCard", () => {
const expectedMetaSelectors = [
".card-priority-badge",
".card-execution-mode-badge",
".card-agent-created-badge",
];
expectedMetaSelectors.forEach((selector) => {
@@ -2637,10 +2636,15 @@ describe("TaskCard", () => {
// longer renders a per-card badge (FN-7539) — an inherited default is not
// meaningfully-configured oversight, so it does not appear among the
// opt-in meta badges here.
const agentBadge = container.querySelector(".card-agent-created-badge");
expect(agentBadge).not.toBeNull();
expect(agentBadge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(agentBadge?.closest(".card-meta-badges")).toBeNull();
expect(agentBadge?.closest(".card-header")).toBeNull();
expect(Array.from(group?.children ?? []).map((child) => child.className)).toEqual([
"card-priority-badge card-priority-badge--high",
"card-execution-mode-badge card-execution-mode-badge--fast",
"card-agent-created-badge",
]);
});
@@ -2663,7 +2667,8 @@ describe("TaskCard", () => {
expect(group).not.toBeNull();
expect(group?.querySelector(".card-priority-badge")).not.toBeNull();
expect(group?.querySelector(".card-execution-mode-badge")).not.toBeNull();
expect(group?.querySelector(".card-agent-created-badge")).not.toBeNull();
expect(group?.querySelector(".card-agent-created-badge")).toBeNull();
expect(container.querySelector(".card-agent-created-badge")?.closest(".card-agent-badge-row")).not.toBeNull();
expect(group?.querySelector(".card-time-indicator")).toBeNull();
expect(container.querySelector(".card-footer-row")).toBeNull();
expect(container.querySelector(".card-footer-row-right")).toBeNull();
@@ -2706,6 +2711,7 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-priority-badge")).toBeNull();
expect(container.querySelector(".card-execution-mode-badge")).toBeNull();
expect(container.querySelector(".card-agent-created-badge")).toBeNull();
expect(container.querySelector(".card-agent-badge-row")).toBeNull();
});
@@ -4497,7 +4503,10 @@ describe("TaskCard", () => {
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(badge?.closest(".card-header")).toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: Task Robot");
expect(badge?.getAttribute("aria-label")).toBe("Created by agent: Task Robot");
expect(badge?.querySelector("span[aria-hidden='true']")?.textContent).toBe("Task Robot");
expect(badge?.querySelector(".visually-hidden")?.textContent).toBe("Created by agent: Task Robot");
});
@@ -4518,6 +4527,8 @@ describe("TaskCard", () => {
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(badge?.closest(".card-meta-badges")).toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: Scheduler Bot");
expect(badge?.getAttribute("aria-label")).toBe("Created by agent: Scheduler Bot");
});
@@ -4539,6 +4550,7 @@ describe("TaskCard", () => {
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: Legacy Robot");
expect(badge?.querySelector("span[aria-hidden='true']")?.textContent).toBe("Legacy Robot");
});
@@ -4559,7 +4571,9 @@ describe("TaskCard", () => {
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent");
expect(badge?.getAttribute("aria-label")).toBe("Created by agent");
expect(badge?.querySelector("span[aria-hidden='true']")?.textContent).toBe("Agent");
expect(badge?.querySelector(".visually-hidden")?.textContent).toBe("Created by agent");
});
@@ -4579,6 +4593,7 @@ describe("TaskCard", () => {
);
expect(container.querySelector(".card-agent-created-badge")).toBeNull();
expect(container.querySelector(".card-agent-badge-row")).toBeNull();
});
it("coexists with GitHub badge and timer metadata", () => {
@@ -4606,7 +4621,7 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-github-badge")).not.toBeNull();
expect(container.querySelector(".card-source-provenance")).not.toBeNull();
expect(container.querySelector(".card-agent-created-badge")).not.toBeNull();
expect(container.querySelector(".card-agent-created-badge")?.closest(".card-agent-badge-row")).not.toBeNull();
expect(container.querySelector(".card-time-indicator")).not.toBeNull();
});
@@ -5813,7 +5828,7 @@ describe("TaskCard workflow badges", () => {
expect(screen.queryByTestId("card-workflow-badge-row")).toBeNull();
});
it("keeps top badges in the meta cluster while rendering workflow identity below card rows", () => {
it("keeps top badges in the meta cluster while rendering agent and workflow identity below card rows", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-30T12:10:00.000Z"));
@@ -5839,11 +5854,16 @@ describe("TaskCard workflow badges", () => {
const metaBadges = screen.getByTestId("card-meta-badges");
const badge = screen.getByTestId("card-workflow-badge");
const workflowRow = screen.getByTestId("card-workflow-badge-row");
const agentRow = screen.getByTestId("card-agent-badge-row");
const agentBadge = container.querySelector(".card-agent-created-badge");
expect(metaBadges.querySelector(".card-priority-badge")).not.toBeNull();
expect(metaBadges.querySelector(".card-execution-mode-badge")).not.toBeNull();
expect(metaBadges.querySelector(".card-agent-created-badge")).not.toBeNull();
expect(metaBadges.querySelector(".card-agent-created-badge")).toBeNull();
expect(metaBadges.querySelector(".card-workflow-badge")).toBeNull();
expect(agentRow).toContainElement(agentBadge as HTMLElement);
expect(agentBadge?.closest(".card-header")).toBeNull();
expect(workflowRow).toContainElement(badge);
expect(agentRow.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
[".card-footer-row", ".card-meta", ".card-agent-row"].forEach((selector) => {
const row = container.querySelector(selector);