diff --git a/.changeset/fn-7780-agent-badge-bottom-row.md b/.changeset/fn-7780-agent-badge-bottom-row.md new file mode 100644 index 0000000000..c997678d6b --- /dev/null +++ b/.changeset/fn-7780-agent-badge-bottom-row.md @@ -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. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f546f2068e..b1fa2f5bfa 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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. + diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index d22d253ddc..f7ba6df3ad 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -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; diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 8ed49fd930..cb3f3c8885 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -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({ {t("tasks.fastMode", "Fast mode")} )} - {isAgentCreated && ( - - - )} {showOversightBadge && ( ]}`} @@ -3544,6 +3532,23 @@ function TaskCardComponent({ {showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()} )} + {isAgentCreated && ( +
+ {/** + * 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. + */} + + +
+ )} {hasWorkflowBadge && (
{/* diff --git a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx index 26bd13d82a..8d23797502 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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"); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 841d85b1d4..d6954751a0 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -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( { 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);