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:
7
.changeset/fn-7780-agent-badge-bottom-row.md
Normal file
7
.changeset/fn-7780-agent-badge-bottom-row.md
Normal 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.
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
{/*
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user