diff --git a/.changeset/fn-8636-card-cost-badge-dash.md b/.changeset/fn-8636-card-cost-badge-dash.md new file mode 100644 index 0000000000..a554c84c6d --- /dev/null +++ b/.changeset/fn-8636-card-cost-badge-dash.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Hide unavailable cost badges on board task cards. +category: fix +dev: Board cards now omit unavailable costs while detail and analytics surfaces retain the guess-free sentinel. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 381d4bc993..a336883801 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1158,13 +1158,15 @@ function TaskCardComponent({ const retryWarningThreshold = useRetryWarning(); const costBadge = useCostBadge(); /* - FNXC:TaskCardCostBadge 2026-07-11-12:20: - The optional spend chip sits beside execution time but stays fully absent unless the default-off setting is enabled and the task has positive token usage. Use the shared read-time taskTokenCost helper so unpriced models show the guess-free “—” sentinel instead of a fabricated $0. + FNXC:TaskCardCostBadge 2026-07-31-23:39: + Board cards must omit the optional spend chip entirely when the derived cost is unavailable, avoiding a dash-only badge or empty Promote row. Detail and analytics surfaces retain formatCost's guess-free “—” sentinel rather than fabricating a $0 value. */ const cardCost = costBadge.enabled && hasTaskCost(task as TaskDetail) ? taskTotalCost(task as TaskDetail, costBadge.pricingOverrides) : null; - const cardCostLabel = cardCost ? formatCost(cardCost.usd, cardCost.unavailable) : null; + const cardCostLabel = cardCost && !cardCost.unavailable && cardCost.usd !== null + ? formatCost(cardCost.usd, false) + : null; // Touch gesture detection refs const touchStartPosRef = useRef<{ x: number; y: number; time: number } | null>(null); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.cost-badge.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.cost-badge.test.tsx index 50d8f7040c..744f5ce015 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.cost-badge.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.cost-badge.test.tsx @@ -108,33 +108,74 @@ describe("TaskCard cost badge", () => { expect(badges[0]?.closest(".card-footer-row-right")).not.toBeNull(); }); - it("keeps unpriced usage guess-free and renders once below Promote", () => { - const { container } = render( - - - , - ); + it.each([1280, 390])("omits unavailable cost chips and their shells at %ipx with or without Promote", (width) => { + const originalWidth = window.innerWidth; + Object.defineProperty(window, "innerWidth", { configurable: true, value: width }); - const badges = container.querySelectorAll(".card-cost-indicator"); - expect(badges).toHaveLength(1); - expect(badges[0]?.textContent).toBe("—"); - expect(badges[0]?.closest(".card-promote-cost-row")).not.toBeNull(); - expect(badges[0]?.closest(".card-footer-row-right")).toBeNull(); + try { + const unavailableWithoutPromote = render( + + + , + ); + expect(unavailableWithoutPromote.container.querySelector(".card-cost-indicator")).toBeNull(); + expect(unavailableWithoutPromote.container.querySelector(".card-cost-indicator[aria-label]")).toBeNull(); + expect(unavailableWithoutPromote.container.querySelector(".card-promote-cost-row")).toBeNull(); + unavailableWithoutPromote.unmount(); + + const unavailableWithPromote = render( + + + , + ); + expect(unavailableWithPromote.container.querySelector(".card-cost-indicator")).toBeNull(); + expect(unavailableWithPromote.container.querySelector(".card-cost-indicator[aria-label]")).toBeNull(); + expect(unavailableWithPromote.container.querySelector(".card-promote-cost-row")).toBeNull(); + unavailableWithPromote.unmount(); + + const pricedWithoutPromote = render( + + + , + ); + const footerBadge = pricedWithoutPromote.container.querySelectorAll(".card-cost-indicator"); + expect(footerBadge).toHaveLength(1); + expect(footerBadge[0]?.textContent).toContain("$"); + pricedWithoutPromote.unmount(); + + const pricedWithPromote = render( + + + , + ); + const promoteBadge = pricedWithPromote.container.querySelectorAll(".card-cost-indicator"); + expect(promoteBadge).toHaveLength(1); + expect(promoteBadge[0]?.textContent).toContain("$"); + expect(promoteBadge[0]?.closest(".card-promote-cost-row")).not.toBeNull(); + } finally { + Object.defineProperty(window, "innerWidth", { configurable: true, value: originalWidth }); + } }); it("keeps the shared card cost chip visible at the mobile breakpoint", () => { diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 0fcbd36fcd..a7a8e99fe5 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -6617,7 +6617,7 @@ describe("TaskCard", () => { expect(container.querySelector(".card-footer-row")).toBeNull(); }); - it("places the unavailable cost sentinel inside todo meta without adding an icon", () => { + it("omits an unavailable cost chip and its footer shell", () => { const { container } = render( { , ); - const costBadge = container.querySelector(".card-cost-indicator") as HTMLElement | null; - expect(costBadge).not.toBeNull(); - expect(costBadge?.textContent).toContain("—"); - expect(costBadge?.querySelector("svg")).toBeNull(); - expect(costBadge?.getAttribute("aria-label")).toBe("Estimated cost —"); - expect(costBadge?.getAttribute("title")).toBe("Estimated cost —"); - expect(costBadge?.closest(".card-meta")).toBe(container.querySelector(".card-meta")); - expect(costBadge?.closest(".card-footer-row")).toBeNull(); + expect(container.querySelector(".card-cost-indicator")).toBeNull(); + expect(container.querySelector(".card-cost-indicator[aria-label]")).toBeNull(); + expect(container.querySelector(".card-promote-cost-row")).toBeNull(); + expect(container.querySelector(".card-footer-row-right")).toBeNull(); expect(container.querySelector(".card-footer-row")).toBeNull(); });