diff --git a/.changeset/fn-6413-task-card-timing-footer.md b/.changeset/fn-6413-task-card-timing-footer.md
new file mode 100644
index 0000000000..28ec81aae3
--- /dev/null
+++ b/.changeset/fn-6413-task-card-timing-footer.md
@@ -0,0 +1,5 @@
+---
+"@runfusion/fusion": patch
+---
+
+Move task-card timing badges from the top metadata cluster into the bottom-right footer chip cluster so timers align with retry and GitHub footer badges.
diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css
index 4a52fbb7f5..bc035b2324 100644
--- a/packages/dashboard/app/components/TaskCard.css
+++ b/packages/dashboard/app/components/TaskCard.css
@@ -760,10 +760,10 @@
align-items: center;
}
-.card-footer-row > .card-time-indicator:first-of-type {
- margin-left: auto;
-}
-
+/*
+FNXC:TaskCardTimingBadge 2026-06-13-17:26:
+The execution-time badge is part of the footer's bottom-right chip cluster, so it inherits the same height, padding, font, and responsive sizing as retry and GitHub tracking badges instead of using the old meta-row placement.
+*/
.card-time-indicator,
.card-github-tracking-chip,
.card-retry-badge,
diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx
index 696b7346bc..b45013dea9 100644
--- a/packages/dashboard/app/components/TaskCard.tsx
+++ b/packages/dashboard/app/components/TaskCard.tsx
@@ -1789,8 +1789,7 @@ function TaskCardComponent({
&& Boolean(githubTrackedIssue);
const hasCardMetaBadges = showPriorityBadge
|| task.executionMode === "fast"
- || isAgentCreated
- || timeIndicator != null;
+ || isAgentCreated;
if (isEditing) {
return (
@@ -2012,16 +2011,6 @@ function TaskCardComponent({
{agentCreatedVisibleLabel}
)}
- {timeIndicator && (
-
-
- {timeIndicator.label}
-
- )}
)}
{task.noCommitsExpected === true && (
@@ -2289,7 +2278,7 @@ function TaskCardComponent({
>
);
})()}
- {(filesChangedButton || isGitHubImportedTask || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
+ {(filesChangedButton || isGitHubImportedTask || timeIndicator || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
{filesChangedButton}
{isGitHubImportedTask && !showLinkedIssueChipForImport && (
@@ -2301,7 +2290,7 @@ function TaskCardComponent({
)}
- {(showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
+ {(timeIndicator || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
{showNearDuplicateChip && (
<>
@@ -2374,6 +2363,20 @@ function TaskCardComponent({
{`#${githubTrackedIssue.number}`}
)}
+ {/*
+ FNXC:TaskCardTimingBadge 2026-06-13-17:20:
+ The execution-time badge belongs in the bottom-right footer cluster and must match sibling footer badge sizing while preserving its existing label, title, aria text, and live-update data.
+ */}
+ {timeIndicator && (
+
+
+ {timeIndicator.label}
+
+ )}
)}
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.footer-alignment.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.footer-alignment.test.tsx
index f2691558a4..6694fde809 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.footer-alignment.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.footer-alignment.test.tsx
@@ -125,7 +125,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
}
});
- it("keeps github and retry right-aligned while timer joins card meta badges", () => {
+ it("keeps github, retry, and timer right-aligned in the footer cluster", () => {
const { container } = render(
{
column: "in-review",
retrySummary: { total: 2 },
executionStartedAt: "2026-05-12T00:00:00.000Z",
+ executionCompletedAt: "2026-05-12T00:05:00.000Z",
updatedAt: "2026-05-12T00:05:00.000Z",
}}
onOpenDetail={noop}
@@ -146,14 +147,14 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement;
const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
- const timerChip = container.querySelector(".card-meta-badges > .card-time-indicator") as HTMLElement;
+ const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy();
expect(rightCluster).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(retryChip).toBeTruthy();
expect(timerChip).toBeTruthy();
- expect(rightCluster.contains(timerChip)).toBe(false);
+ expect(rightCluster.contains(timerChip)).toBe(true);
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
@@ -170,6 +171,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
sourceType: "github_import",
retrySummary: { total: 3 },
executionStartedAt: "2026-05-12T00:00:00.000Z",
+ executionCompletedAt: "2026-05-12T00:05:00.000Z",
updatedAt: "2026-05-12T00:05:00.000Z",
}}
onOpenDetail={noop}
@@ -183,7 +185,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement;
const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
- const timerChip = container.querySelector(".card-meta-badges > .card-time-indicator") as HTMLElement;
+ const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy();
expect(sourceChip).toBeTruthy();
@@ -191,13 +193,14 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(timerChip).toBeTruthy();
- expect(rightCluster.contains(timerChip)).toBe(false);
+ expect(rightCluster.contains(timerChip)).toBe(true);
expect(getComputedStyle(sourceChip).marginLeft).not.toBe("auto");
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
expect(Array.from(rightCluster.children).map((node) => (node as HTMLElement).className)).toEqual([
"card-github-tracking-chip card-github-tracking-link",
expect.stringContaining("card-retry-badge"),
+ "card-time-indicator",
]);
});
});
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx
index 9c139274cc..62840c5456 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx
@@ -124,6 +124,7 @@ describe("TaskCard footer wrapping (FN-5210)", () => {
expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(timeChip).toBeTruthy();
+ expect(rightCluster.contains(timeChip)).toBe(true);
const footerStyles = getComputedStyle(footerRow);
expect(footerStyles.flexWrap).toBe("wrap");
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
index c56e09db53..75e346be9b 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
@@ -137,6 +137,16 @@ function mountCssForBadgeTests() {
};
}
+function expectTimerInFooterRight(container: HTMLElement) {
+ const timer = container.querySelector(".card-time-indicator");
+ const footerRow = container.querySelector(".card-footer-row");
+ const rightCluster = container.querySelector(".card-footer-row-right");
+ expect(timer).not.toBeNull();
+ expect(footerRow?.contains(timer)).toBe(true);
+ expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
+ expect(timer?.closest(".card-meta-badges")).toBeNull();
+}
+
const highFanout = {
totalCount: 7,
activeTodoCount: 3,
@@ -1602,7 +1612,9 @@ describe("TaskCard", () => {
expect(screen.getByTestId("icon-zap")).toBeDefined();
});
- it("groups priority, fast mode, agent-created, and time metadata in one badge row", () => {
+
+ it("keeps priority, fast mode, and agent-created in meta while time moves to footer", () => {
+
const { container } = render(
{
const group = container.querySelector(".card-meta-badges");
expect(group).not.toBeNull();
- const expectedSelectors = [
+
+ const expectedMetaSelectors = [
".card-priority-badge",
".card-execution-mode-badge",
".card-agent-created-badge",
- ".card-time-indicator",
];
- expectedSelectors.forEach((selector) => {
+ expectedMetaSelectors.forEach((selector) => {
+
const badge = container.querySelector(selector);
expect(badge).not.toBeNull();
expect(badge?.closest(".card-meta-badges")).toBe(group);
});
+
+ const timer = container.querySelector(".card-time-indicator");
+ expect(timer).not.toBeNull();
+ expect(timer?.closest(".card-meta-badges")).toBeNull();
+ expect(timer?.closest(".card-footer-row-right")).not.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",
- "card-time-indicator",
+
+
]);
});
@@ -1665,7 +1685,9 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-footer-row-right")).toBeNull();
});
- it("moves a lone time chip into card meta badges without rendering an empty footer", () => {
+
+ it("moves a lone time chip into the footer without rendering empty meta badges", () => {
+
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-25T12:05:00.000Z"));
@@ -1683,14 +1705,19 @@ describe("TaskCard", () => {
const group = container.querySelector(".card-meta-badges");
const timer = container.querySelector(".card-time-indicator");
- expect(group).not.toBeNull();
+
+ const footerRow = container.querySelector(".card-footer-row");
+ const rightCluster = container.querySelector(".card-footer-row-right");
+ expect(group).toBeNull();
expect(timer).not.toBeNull();
- expect(timer?.closest(".card-meta-badges")).toBe(group);
+ expect(footerRow).not.toBeNull();
+ expect(rightCluster).not.toBeNull();
+ expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
+ expect(Array.from(rightCluster?.children ?? [])).toEqual([timer]);
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-footer-row")).toBeNull();
- expect(container.querySelector(".card-footer-row-right")).toBeNull();
+
});
it("does not render card meta badge shells when all grouped affordances are absent", () => {
@@ -2335,7 +2362,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
// 8m workflow + 4m timed = 12m
expect(timer?.textContent).toContain("12m");
expect(timer?.getAttribute("title")).toContain("In progress 12m");
@@ -2354,6 +2381,7 @@ describe("TaskCard", () => {
);
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m");
+ expectTimerInFooterRight(container);
rerender(
{
);
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("2m");
+ expectTimerInFooterRight(container);
});
it("shows timer chip for done cards summing workflow runtime + timed events", () => {
@@ -2401,7 +2430,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
// 1h workflow + 1h timed = 2h
expect(timer?.textContent).toContain("2h");
expect(timer?.getAttribute("title")).toContain("Execution time 2h");
@@ -2512,7 +2541,9 @@ describe("TaskCard", () => {
expect(queuedBadge?.compareDocumentPosition(footerRow as Node) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy();
});
- it("renders tracking and retry in the footer while timer joins the card meta badges", () => {
+
+ it("renders tracking, retry, and timer in the footer right cluster", () => {
+
const { container } = render(
{
const footerRow = container.querySelector(".card-footer-row");
const metaBadges = container.querySelector(".card-meta-badges");
+
+ const rightCluster = container.querySelector(".card-footer-row-right");
+
const trackingLink = container.querySelector(".card-github-tracking-chip");
const retryChip = container.querySelector(".card-retry-badge");
const timerChip = container.querySelector(".card-time-indicator");
expect(footerRow).not.toBeNull();
- expect(metaBadges).not.toBeNull();
+
+ expect(metaBadges).toBeNull();
+ expect(rightCluster).not.toBeNull();
expect(footerRow?.contains(trackingLink)).toBe(true);
expect(footerRow?.contains(retryChip)).toBe(true);
- expect(footerRow?.contains(timerChip)).toBe(false);
- expect(metaBadges?.contains(timerChip)).toBe(true);
+ expect(footerRow?.contains(timerChip)).toBe(true);
+ expect(rightCluster?.contains(timerChip)).toBe(true);
+ expect(Array.from(rightCluster?.children ?? [])).toContain(timerChip);
+
expect(container.querySelector(".card-bottom-right-row")).toBeNull();
});
@@ -2745,7 +2783,9 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-footer-row > .card-source-provenance")).toBeNull();
});
- it("keeps github badges before retry while time chip joins card meta badges", () => {
+
+ it("keeps github badges before retry while time chip ends the footer cluster", () => {
+
const { container } = render(
{
const timerChip = container.querySelector(".card-time-indicator");
expect(sourceNode).not.toBeNull();
expect(rightCluster).not.toBeNull();
- expect(timerChip?.closest(".card-meta-badges")).not.toBeNull();
+
+ expect(timerChip?.closest(".card-footer-row-right")).toBe(rightCluster);
const orderedNodes = [
rightCluster?.querySelector(".card-github-tracking-chip"),
rightCluster?.querySelector(".card-retry-badge"),
+ timerChip,
+
];
orderedNodes.forEach((node) => expect(node).not.toBeNull());
expect(Array.from((rightCluster as Element).children)).toEqual(orderedNodes);
@@ -2838,17 +2881,15 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
- if (rightSideChip?.classList.contains("card-time-indicator")) {
- expect(children.at(-1)).toBe(trackingChip);
- expect(rightSideChip.closest(".card-meta-badges")).not.toBeNull();
- } else {
- expect(children.at(-1)).toBe(rightSideChip);
- expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
- }
+
+ expect(children).toContain(trackingChip as HTMLElement);
+ expect(children).toContain(rightSideChip as HTMLElement);
+ expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
});
- it.each(["in-progress", "in-review"] as const)("renders time indicator in meta badges beside footer tracking chip for %s", (column) => {
+ it.each(["in-progress", "in-review"] as const)("renders time indicator in footer right cluster beside tracking chip for %s", (column) => {
+
const { container } = render(
{
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
expect(children).toContain(trackingChip);
- expect(children).not.toContain(timeChip);
- expect(timeChip?.closest(".card-meta-badges")).not.toBeNull();
+
+ expect(children).toContain(timeChip);
+ expect(timeChip?.closest(".card-footer-row-right")).toBe(rightCluster);
+
});
it("does not force far-right modifier when in-progress card has files changed", () => {
@@ -2940,13 +2983,11 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
- if (rightSideChip?.classList.contains("card-time-indicator")) {
- expect(children.at(-1)).toBe(trackingChip);
- expect(rightSideChip.closest(".card-meta-badges")).not.toBeNull();
- } else {
- expect(children.at(-1)).toBe(rightSideChip);
- expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
- }
+
+ expect(children).toContain(trackingChip as HTMLElement);
+ expect(children).toContain(rightSideChip as HTMLElement);
+ expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
+
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
});
@@ -3010,8 +3051,10 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
- expect((trackingChip as HTMLElement).nextElementSibling).toBeNull();
- expect(timerChip?.closest(".card-meta-badges")).not.toBeNull();
+
+ expect((trackingChip as HTMLElement).nextElementSibling).toBe(timerChip);
+ expect(timerChip?.closest(".card-footer-row-right")).toBe(rightCluster);
+
} finally {
cleanupCss();
}
@@ -3798,12 +3841,15 @@ describe("TaskCard", () => {
expect(footerRow).not.toBeNull();
expect(filesChanged).not.toBeNull();
expect(timer).not.toBeNull();
+ const rightCluster = container.querySelector(".card-footer-row-right");
expect(footerRow?.contains(filesChanged)).toBe(true);
- expect(footerRow?.contains(timer)).toBe(false);
- expect(header?.contains(timer)).toBe(true);
- expect(timer?.closest(".card-meta-badges")).not.toBeNull();
- expect(container.querySelector(".card-footer-row-right")).toBeNull();
- expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged]);
+
+ expect(footerRow?.contains(timer)).toBe(true);
+ expect(header?.contains(timer)).toBe(false);
+ expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
+ expect(rightCluster).not.toBeNull();
+ expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged, rightCluster]);
+
});
it("shows timer chip for in-review cards", () => {
@@ -3835,7 +3881,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("12m");
expect(timer?.getAttribute("title")).toContain("Execution time 12m");
expect(timer?.getAttribute("title")).not.toContain("Completed");
@@ -3859,7 +3905,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("30m");
expect(timer?.getAttribute("title")).toBe("Execution time 30m");
@@ -3891,7 +3937,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("6m");
expect(timer?.getAttribute("title")).toBe("Execution time 6m");
});
@@ -3945,7 +3991,7 @@ describe("TaskCard", () => {
);
const timer = container.querySelector(".card-time-indicator");
- expect(timer).not.toBeNull();
+ expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("45m");
expect(timer?.getAttribute("title")).toBe("Execution time 45m. Merge phase <1m");
} finally {
diff --git a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx
index 614627a987..cd34ec435e 100644
--- a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx
@@ -339,9 +339,10 @@ describe("TaskCard mobile", () => {
expectRuleToContain(css, ".card-footer-row", "row-gap: var(--space-xs);");
});
- it("keeps TaskCard footer-chip cluster anchored by first chip rules", () => {
+ it("keeps TaskCard footer-chip cluster anchored by the right wrapper", () => {
const css = loadAllAppCss();
- expectRuleToContain(css, ".card-footer-row > .card-time-indicator:first-of-type", "margin-left: auto;");
+ expectRuleToContain(css, ".card-footer-row-right", "margin-left: auto;");
+ expect(css).not.toContain(".card-footer-row > .card-time-indicator:first-of-type");
const timeIndicatorRule = css.match(/\.card-time-indicator\s*\{[^}]*\}/)?.[0] ?? "";
expect(timeIndicatorRule).not.toContain("margin-left: auto;");