FN-8636: hide unavailable task card cost badges

Hide dash-only cost badges from board task cards when pricing is unavailable.

- Suppress unavailable cost labels and their empty layout shells.
- Cover priced and unavailable badges with and without Promote across desktop and mobile widths.
- Add a patch changeset for the board-card fix.

Files changed:
 .changeset/fn-8636-card-cost-badge-dash.md         |  7 ++
 packages/dashboard/app/components/TaskCard.tsx     |  8 +-
 .../__tests__/TaskCard.cost-badge.test.tsx         | 93 ++++++++++++++++------
 .../app/components/__tests__/TaskCard.test.tsx     | 14 ++--
 4 files changed, 84 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8636

Fusion-Task-Lineage: 31fbf82a-7a67-4629-bf82-48faf3c3a9d7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-31 16:50:24 -07:00
parent 91c3854607
commit 5bf9279c5d
4 changed files with 84 additions and 38 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -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(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard
task={taskWithUsage({
tokenUsage: {
...taskWithUsage().tokenUsage!,
modelProvider: "unknown",
modelId: "no-price",
perModel: [
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 },
{ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 },
],
},
})}
onOpenDetail={noop}
addToast={noop}
onPromote={vi.fn().mockResolvedValue(undefined)}
/>
</CostBadgeProvider>,
);
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(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard
task={taskWithUsage({
tokenUsage: { ...taskWithUsage().tokenUsage!, modelProvider: "unknown", modelId: "no-price" },
})}
onOpenDetail={noop}
addToast={noop}
/>
</CostBadgeProvider>,
);
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(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard
task={taskWithUsage({
tokenUsage: {
...taskWithUsage().tokenUsage!,
modelProvider: "unknown",
modelId: "no-price",
perModel: [
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 },
{ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 },
],
},
})}
onOpenDetail={noop}
addToast={noop}
onPromote={vi.fn().mockResolvedValue(undefined)}
/>
</CostBadgeProvider>,
);
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(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard task={taskWithUsage()} onOpenDetail={noop} addToast={noop} />
</CostBadgeProvider>,
);
const footerBadge = pricedWithoutPromote.container.querySelectorAll(".card-cost-indicator");
expect(footerBadge).toHaveLength(1);
expect(footerBadge[0]?.textContent).toContain("$");
pricedWithoutPromote.unmount();
const pricedWithPromote = render(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard task={taskWithUsage()} onOpenDetail={noop} addToast={noop} onPromote={vi.fn().mockResolvedValue(undefined)} />
</CostBadgeProvider>,
);
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", () => {

View File

@@ -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(
<CostBadgeProvider value={{ enabled: true }}>
<TaskCard
@@ -6642,14 +6642,10 @@ describe("TaskCard", () => {
</CostBadgeProvider>,
);
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();
});